optimize json
This commit is contained in:
parent
5a7e179b05
commit
be135cb02a
7 changed files with 886 additions and 561 deletions
11
dist/css/styles.css
vendored
11
dist/css/styles.css
vendored
|
|
@ -1409,6 +1409,17 @@ select:focus {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error-state{
|
||||||
|
padding: 0.5em;
|
||||||
|
background-color: #ff97e054;
|
||||||
|
color: #842029;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.error-dataurl{
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
.loading-state {
|
.loading-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
6
dist/index.html
vendored
6
dist/index.html
vendored
|
|
@ -40,7 +40,7 @@
|
||||||
<link rel="prefetch" href="/json/file-hosts.json" crossorigin>
|
<link rel="prefetch" href="/json/file-hosts.json" crossorigin>
|
||||||
<link rel="prefetch" href="/json/adult-hosts.json" crossorigin>
|
<link rel="prefetch" href="/json/adult-hosts.json" crossorigin>
|
||||||
<link rel="prefetch" href="/json/pricing.json" crossorigin>
|
<link rel="prefetch" href="/json/pricing.json" crossorigin>
|
||||||
<link as="style" rel="preload" href="css/styles.css">
|
<link as="style" rel="preload" href="./css/styles-min.css">
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
|
@ -73,9 +73,9 @@
|
||||||
<meta name="twitter:image" content="https://debrid-services-comparison.netlify.app/image.png">
|
<meta name="twitter:image" content="https://debrid-services-comparison.netlify.app/image.png">
|
||||||
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="css/styles.css">
|
<link rel="stylesheet" href="./css/styles-min.css">
|
||||||
|
|
||||||
<script src="js/app.js" defer></script>
|
<script src="js/app-min.js" defer></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
||||||
1
dist/js/app-min.js
vendored
Normal file
1
dist/js/app-min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
569
dist/js/app.js
vendored
569
dist/js/app.js
vendored
|
|
@ -11,69 +11,59 @@
|
||||||
Utils
|
Utils
|
||||||
============================== */
|
============================== */
|
||||||
const Utils = (() => {
|
const Utils = (() => {
|
||||||
const debounceCache = new WeakMap();
|
const debounceMap = new WeakMap();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
debounce(func, delay = 300, immediate = false) {
|
debounce(func, wait = 300, immediate = false) {
|
||||||
if (debounceCache.has(func)) return debounceCache.get(func);
|
if (debounceMap.has(func)) return debounceMap.get(func);
|
||||||
|
let timeout;
|
||||||
let timeoutId;
|
|
||||||
const debounced = function (...args) {
|
const debounced = function (...args) {
|
||||||
const callNow = immediate && !timeoutId;
|
const callNow = immediate && !timeout;
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeout);
|
||||||
|
|
||||||
if (callNow) func.apply(this, args);
|
if (callNow) func.apply(this, args);
|
||||||
|
timeout = setTimeout(() => {
|
||||||
timeoutId = setTimeout(() => {
|
timeout = null;
|
||||||
timeoutId = null;
|
|
||||||
if (!immediate) func.apply(this, args);
|
if (!immediate) func.apply(this, args);
|
||||||
}, delay);
|
}, wait);
|
||||||
};
|
};
|
||||||
|
debounceMap.set(func, debounced);
|
||||||
debounceCache.set(func, debounced);
|
|
||||||
return debounced;
|
return debounced;
|
||||||
},
|
},
|
||||||
|
|
||||||
throttle(func, limit = 100) {
|
throttle(func, limit = 100) {
|
||||||
let ticking = false;
|
let inThrottle = false;
|
||||||
return function (...args) {
|
return function (...args) {
|
||||||
if (!ticking) {
|
if (!inThrottle) {
|
||||||
func.apply(this, args);
|
func.apply(this, args);
|
||||||
ticking = true;
|
inThrottle = true;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
setTimeout(() => { ticking = false; }, limit);
|
setTimeout(() => inThrottle = false, limit);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
animateOnScroll: (() => {
|
animateOnScroll: (() => {
|
||||||
let observer;
|
let observer;
|
||||||
return (elements, options = {}) => {
|
return (elements, options = {}) => {
|
||||||
if (!('IntersectionObserver' in window)) return;
|
if (!('IntersectionObserver' in window)) return;
|
||||||
if (!observer) {
|
if (!observer) {
|
||||||
observer = new IntersectionObserver((entries) => {
|
observer = new IntersectionObserver((entries) => {
|
||||||
entries.forEach(entry => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
entry.target.classList.add('animate-in');
|
entry.target.classList.add("animate-in");
|
||||||
observer.unobserve(entry.target);
|
observer.unobserve(entry.target);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, {
|
}, {
|
||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
rootMargin: '0px 0px -50px 0px',
|
rootMargin: "0px 0px -50px 0px",
|
||||||
...options
|
...options
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
elements.forEach(el => observer.observe(el));
|
elements.forEach((element) => observer.observe(element));
|
||||||
};
|
};
|
||||||
})()
|
})()
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/* ==============================
|
|
||||||
Loading Manager
|
|
||||||
============================== */
|
|
||||||
class LoadingManager {
|
class LoadingManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.activeLoaders = new Map();
|
this.activeLoaders = new Map();
|
||||||
|
|
@ -81,7 +71,7 @@ class LoadingManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
createTemplate() {
|
createTemplate() {
|
||||||
const template = document.createElement('template');
|
const template = document.createElement("template");
|
||||||
template.innerHTML = `
|
template.innerHTML = `
|
||||||
<div class="loading-overlay">
|
<div class="loading-overlay">
|
||||||
<div class="loading-content">
|
<div class="loading-content">
|
||||||
|
|
@ -97,42 +87,41 @@ class LoadingManager {
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
show(target, text = 'Loading...') {
|
show(target, text = "Loading...") {
|
||||||
const targetEl = typeof target === 'string' ? document.querySelector(target) : target;
|
const element = typeof target === "string" ? document.querySelector(target) : target;
|
||||||
if (!targetEl) return null;
|
if (!element) return null;
|
||||||
|
|
||||||
const loaderId = `loader-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
const loaderId = `loader-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
const loader = this.template.content.cloneNode(true).firstElementChild;
|
const loaderElement = this.template.content.cloneNode(true).firstElementChild;
|
||||||
|
loaderElement.dataset.loaderId = loaderId;
|
||||||
|
loaderElement.querySelector(".loading-text").textContent = text;
|
||||||
|
|
||||||
loader.dataset.loaderId = loaderId;
|
const computedStyle = getComputedStyle(element);
|
||||||
loader.querySelector('.loading-text').textContent = text;
|
if (computedStyle.position === "static") {
|
||||||
|
element.style.position = "relative";
|
||||||
|
}
|
||||||
|
|
||||||
const prevPos = getComputedStyle(targetEl).position;
|
element.appendChild(loaderElement);
|
||||||
if (prevPos === 'static' || !prevPos) targetEl.style.position = 'relative';
|
this.activeLoaders.set(loaderId, loaderElement);
|
||||||
|
|
||||||
targetEl.appendChild(loader);
|
requestAnimationFrame(() => loaderElement.classList.add("loading-overlay--visible"));
|
||||||
this.activeLoaders.set(loaderId, loader);
|
|
||||||
|
|
||||||
requestAnimationFrame(() => loader.classList.add('loading-overlay--visible'));
|
|
||||||
return loaderId;
|
return loaderId;
|
||||||
}
|
}
|
||||||
|
|
||||||
hide(target, loaderId) {
|
hide(target, loaderId) {
|
||||||
const loader = this.activeLoaders.get(loaderId);
|
const loaderElement = this.activeLoaders.get(loaderId);
|
||||||
if (!loader) return;
|
if (loaderElement) {
|
||||||
loader.classList.add('loading-overlay--hiding');
|
loaderElement.classList.add("loading-overlay--hiding");
|
||||||
this.activeLoaders.delete(loaderId);
|
this.activeLoaders.delete(loaderId);
|
||||||
setTimeout(() => loader.remove(), 250);
|
setTimeout(() => loaderElement.remove(), 250);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hideAll() {
|
hideAll() {
|
||||||
this.activeLoaders.forEach((_, id) => this.hide(null, id));
|
this.activeLoaders.forEach((_, loaderId) => this.hide(null, loaderId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==============================
|
|
||||||
Table Manager (file/adult hosts)
|
|
||||||
============================== */
|
|
||||||
class TableManager {
|
class TableManager {
|
||||||
constructor(containerId, searchInputId, clearIconId, options = {}) {
|
constructor(containerId, searchInputId, clearIconId, options = {}) {
|
||||||
this.elements = {
|
this.elements = {
|
||||||
|
|
@ -141,74 +130,117 @@ class TableManager {
|
||||||
clearIcon: document.getElementById(clearIconId)
|
clearIcon: document.getElementById(clearIconId)
|
||||||
};
|
};
|
||||||
|
|
||||||
this.options = { sortable: true, filterable: true, pagination: false, itemsPerPage: 50, ...options };
|
this.options = {
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
pagination: false,
|
||||||
|
itemsPerPage: 50,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
currentData: {},
|
currentData: {},
|
||||||
filteredData: {},
|
filteredData: {},
|
||||||
currentSort: { column: null, direction: 'asc' },
|
currentSort: { column: null, direction: "asc" },
|
||||||
currentPage: 1
|
currentPage: 1,
|
||||||
|
services: [], // Store services array for indexed format
|
||||||
|
isIndexedFormat: false // Flag to track data format
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleSearch = Utils.debounce(this._performSearch.bind(this), 250);
|
this.handleSearch = Utils.debounce(this._performSearch.bind(this), 250);
|
||||||
this.handleSort = this._performSort.bind(this);
|
this.handleSort = this._performSort.bind(this);
|
||||||
|
|
||||||
this._init();
|
this._init();
|
||||||
}
|
}
|
||||||
|
|
||||||
_init() {
|
_init() {
|
||||||
if (this.elements.searchInput) {
|
if (this.elements.searchInput) {
|
||||||
this.elements.searchInput.addEventListener('input', this.handleSearch);
|
this.elements.searchInput.addEventListener("input", this.handleSearch);
|
||||||
this.elements.searchInput.addEventListener('keydown', e => {
|
this.elements.searchInput.addEventListener("keydown", (e) => {
|
||||||
if (e.key === 'Escape') this._clearSearch();
|
if (e.key === "Escape") this._clearSearch();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.elements.clearIcon) {
|
if (this.elements.clearIcon) {
|
||||||
this.elements.clearIcon.addEventListener('click', () => this._clearSearch());
|
this.elements.clearIcon.addEventListener("click", () => this._clearSearch());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New method to transform indexed data to regular format
|
||||||
|
_transformIndexedData(indexedData) {
|
||||||
|
const { services, supported } = indexedData;
|
||||||
|
const transformed = {};
|
||||||
|
|
||||||
|
for (const [host, supportedIndices] of Object.entries(supported)) {
|
||||||
|
transformed[host] = {};
|
||||||
|
services.forEach((service, index) => {
|
||||||
|
transformed[host][service] = supportedIndices.includes(index) ? "yes" : "no";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store services for later use
|
||||||
|
this.state.services = services;
|
||||||
|
this.state.isIndexedFormat = true;
|
||||||
|
|
||||||
|
return transformed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modified method to handle both formats
|
||||||
generateTable(data = {}) {
|
generateTable(data = {}) {
|
||||||
if (!this.elements.container) return;
|
if (!this.elements.container) return;
|
||||||
if (!Object.keys(data).length) {
|
|
||||||
|
// Handle both old and new indexed formats
|
||||||
|
let processedData;
|
||||||
|
if (data.services && data.supported) {
|
||||||
|
// New indexed format
|
||||||
|
processedData = this._transformIndexedData(data);
|
||||||
|
} else {
|
||||||
|
// Old format
|
||||||
|
processedData = data;
|
||||||
|
this.state.isIndexedFormat = false;
|
||||||
|
// Try to extract services from first entry
|
||||||
|
const firstHost = Object.values(processedData)[0];
|
||||||
|
this.state.services = firstHost ? Object.keys(firstHost) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.keys(processedData).length) {
|
||||||
this.elements.container.innerHTML = '<div class="empty-state"><p>No data available.</p></div>';
|
this.elements.container.innerHTML = '<div class="empty-state"><p>No data available.</p></div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.currentData = data;
|
this.state.currentData = processedData;
|
||||||
this.state.filteredData = { ...data };
|
this.state.filteredData = { ...processedData };
|
||||||
|
|
||||||
const firstKey = Object.keys(data)[0];
|
const firstHostKey = Object.keys(processedData)[0];
|
||||||
const providers = firstKey ? Object.keys(data[firstKey]) : [];
|
const columns = firstHostKey ? Object.keys(processedData[firstHostKey]) : [];
|
||||||
const tableId = this.elements.container.id.replace('-container', '');
|
const tableId = this.elements.container.id.replace("-container", "");
|
||||||
|
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement("div");
|
||||||
wrapper.className = 'table-wrapper';
|
wrapper.className = "table-wrapper";
|
||||||
|
|
||||||
const table = document.createElement('table');
|
const table = document.createElement("table");
|
||||||
table.id = tableId;
|
table.id = tableId;
|
||||||
table.className = 'enhanced-table';
|
table.className = "enhanced-table";
|
||||||
table.setAttribute('aria-label', 'Service Comparison Table');
|
table.setAttribute("aria-label", "Service Comparison Table");
|
||||||
|
|
||||||
const thead = document.createElement('thead');
|
const thead = document.createElement("thead");
|
||||||
thead.innerHTML = this._generateTableHeader(providers);
|
thead.innerHTML = this._generateTableHeader(columns);
|
||||||
table.appendChild(thead);
|
table.appendChild(thead);
|
||||||
|
|
||||||
const tbody = document.createElement('tbody');
|
const tbody = document.createElement("tbody");
|
||||||
tbody.innerHTML = this._generateTableRows(this.state.filteredData, providers);
|
tbody.innerHTML = this._generateTableRows(this.state.filteredData, columns);
|
||||||
table.appendChild(tbody);
|
table.appendChild(tbody);
|
||||||
|
|
||||||
wrapper.appendChild(table);
|
wrapper.appendChild(table);
|
||||||
fragment.appendChild(wrapper);
|
fragment.appendChild(wrapper);
|
||||||
|
|
||||||
this.elements.container.innerHTML = '';
|
this.elements.container.innerHTML = "";
|
||||||
this.elements.container.appendChild(fragment);
|
this.elements.container.appendChild(fragment);
|
||||||
|
|
||||||
this._attachTableEvents();
|
this._attachTableEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
_generateTableHeader(providers) {
|
_generateTableHeader(columns) {
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<th class="sortable" data-column="service" tabindex="0" role="columnheader" aria-sort="none">
|
<th class="sortable" data-column="service" tabindex="0" role="columnheader" aria-sort="none">
|
||||||
|
|
@ -217,21 +249,21 @@ class TableManager {
|
||||||
<path d="M12 5v14M5 12l7-7 7 7"/>
|
<path d="M12 5v14M5 12l7-7 7 7"/>
|
||||||
</svg>
|
</svg>
|
||||||
</th>
|
</th>
|
||||||
${providers.map(provider => `
|
${columns.map(column => `
|
||||||
<th class="sortable" data-column="${provider}" tabindex="0" role="columnheader" aria-sort="none">
|
<th class="sortable" data-column="${column}" tabindex="0" role="columnheader" aria-sort="none">
|
||||||
<span>${provider}</span>
|
<span>${column}</span>
|
||||||
<svg class="sort-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
<svg class="sort-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||||
<path d="M12 5v14M5 12l7-7 7 7"/>
|
<path d="M12 5v14M5 12l7-7 7 7"/>
|
||||||
</svg>
|
</svg>
|
||||||
</th>
|
</th>
|
||||||
`).join('')}
|
`).join("")}
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
_generateTableRows(data, providers) {
|
_generateTableRows(data, columns) {
|
||||||
const rows = [];
|
const rows = [];
|
||||||
for (const [host, providerData] of Object.entries(data)) {
|
for (const [host, hostData] of Object.entries(data)) {
|
||||||
rows.push(`
|
rows.push(`
|
||||||
<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">
|
||||||
|
|
@ -239,105 +271,118 @@ class TableManager {
|
||||||
<span class="service-name">${host}</span>
|
<span class="service-name">${host}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
${providers.map(provider => {
|
${columns.map(column => {
|
||||||
const isSupported = providerData[provider] === 'yes';
|
const isSupported = hostData[column] === "yes";
|
||||||
return `
|
return `
|
||||||
<td class="status-cell" role="gridcell" data-status="${providerData[provider]}">
|
<td class="status-cell" role="gridcell" data-status="${hostData[column]}">
|
||||||
<span class="status-indicator ${isSupported ? 'supported' : 'not-supported'}" aria-label="${isSupported ? 'Supported' : 'Not supported'}">
|
<span class="status-indicator ${isSupported ? "supported" : "not-supported"}" aria-label="${isSupported ? "Supported" : "Not supported"}">
|
||||||
${isSupported
|
${isSupported ?
|
||||||
? '<svg class="table-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><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></svg>'
|
'<svg class="table-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><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></svg>' :
|
||||||
: '<svg class="table-cross" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><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></svg>'
|
'<svg class="table-cross" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><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></svg>'
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
}).join('')}
|
}).join("")}
|
||||||
</tr>
|
</tr>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
return rows.join('');
|
return rows.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
_attachTableEvents() {
|
_attachTableEvents() {
|
||||||
if (!this.options.sortable) return;
|
if (!this.options.sortable) return;
|
||||||
const thead = this.elements.container.querySelector('thead');
|
|
||||||
|
const thead = this.elements.container.querySelector("thead");
|
||||||
if (thead) {
|
if (thead) {
|
||||||
thead.addEventListener('click', this.handleSort);
|
thead.addEventListener("click", this.handleSort);
|
||||||
thead.addEventListener('keydown', e => {
|
thead.addEventListener("keydown", (e) => {
|
||||||
const target = e.target.closest('.sortable');
|
const sortableHeader = e.target.closest(".sortable");
|
||||||
if (!target) return;
|
if (sortableHeader && (e.key === "Enter" || e.key === " ")) {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.handleSort({ target });
|
this.handleSort({ target: sortableHeader });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_performSearch() {
|
_performSearch() {
|
||||||
const term = (this.elements.searchInput?.value || '').toLowerCase().trim();
|
const searchTerm = (this.elements.searchInput?.value || "").toLowerCase().trim();
|
||||||
|
|
||||||
if (!term) {
|
if (searchTerm) {
|
||||||
this.state.filteredData = { ...this.state.currentData };
|
|
||||||
if (this.elements.clearIcon) this.elements.clearIcon.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
this.state.filteredData = Object.fromEntries(
|
this.state.filteredData = Object.fromEntries(
|
||||||
Object.entries(this.state.currentData).filter(([host]) => host.toLowerCase().includes(term))
|
Object.entries(this.state.currentData).filter(([host]) =>
|
||||||
|
host.toLowerCase().includes(searchTerm)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
if (this.elements.clearIcon) this.elements.clearIcon.style.display = 'block';
|
if (this.elements.clearIcon) {
|
||||||
|
this.elements.clearIcon.style.display = "block";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.state.filteredData = { ...this.state.currentData };
|
||||||
|
if (this.elements.clearIcon) {
|
||||||
|
this.elements.clearIcon.style.display = "none";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._updateTableContent();
|
this._updateTableContent();
|
||||||
this._updateSearchResults(term);
|
this._updateSearchResults(searchTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
_clearSearch() {
|
_clearSearch() {
|
||||||
if (this.elements.searchInput) this.elements.searchInput.value = '';
|
if (this.elements.searchInput) {
|
||||||
|
this.elements.searchInput.value = "";
|
||||||
|
}
|
||||||
this.state.filteredData = { ...this.state.currentData };
|
this.state.filteredData = { ...this.state.currentData };
|
||||||
if (this.elements.clearIcon) this.elements.clearIcon.style.display = 'none';
|
if (this.elements.clearIcon) {
|
||||||
|
this.elements.clearIcon.style.display = "none";
|
||||||
|
}
|
||||||
this._updateTableContent();
|
this._updateTableContent();
|
||||||
this._updateSearchResults('');
|
this._updateSearchResults("");
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateSearchResults(searchTerm) {
|
_updateSearchResults(searchTerm) {
|
||||||
if (!this.elements.container) return;
|
if (!this.elements.container) return;
|
||||||
let indicator = this.elements.container.querySelector('.search-results');
|
|
||||||
|
let resultsElement = this.elements.container.querySelector(".search-results");
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
const resultCount = Object.keys(this.state.filteredData).length;
|
const filteredCount = Object.keys(this.state.filteredData).length;
|
||||||
const totalCount = Object.keys(this.state.currentData).length;
|
const totalCount = Object.keys(this.state.currentData).length;
|
||||||
|
|
||||||
if (!indicator) {
|
if (!resultsElement) {
|
||||||
indicator = document.createElement('div');
|
resultsElement = document.createElement("div");
|
||||||
indicator.className = 'search-results';
|
resultsElement.className = "search-results";
|
||||||
this.elements.container.insertBefore(indicator, this.elements.container.firstChild);
|
this.elements.container.insertBefore(resultsElement, this.elements.container.firstChild);
|
||||||
}
|
}
|
||||||
indicator.textContent = `Showing ${resultCount} of ${totalCount} services`;
|
|
||||||
indicator.style.display = 'block';
|
resultsElement.textContent = `Showing ${filteredCount} of ${totalCount} services`;
|
||||||
indicator.style.textAlign = 'center';
|
resultsElement.style.display = "block";
|
||||||
indicator.style.padding = '0.5em 0';
|
resultsElement.style.textAlign = "center";
|
||||||
} else if (indicator) {
|
resultsElement.style.padding = "0.5em 0";
|
||||||
indicator.style.display = 'none';
|
} else if (resultsElement) {
|
||||||
|
resultsElement.style.display = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_performSort(event) {
|
_performSort(event) {
|
||||||
const header = event.target.closest('.sortable');
|
const sortableHeader = event.target.closest(".sortable");
|
||||||
if (!header) return;
|
if (!sortableHeader) return;
|
||||||
|
|
||||||
const column = header.dataset.column;
|
const column = sortableHeader.dataset.column;
|
||||||
const { currentSort } = this.state;
|
const { currentSort } = this.state;
|
||||||
const newDirection = currentSort.column === column && currentSort.direction === 'asc' ? 'desc' : 'asc';
|
const direction = currentSort.column === column && currentSort.direction === "asc" ? "desc" : "asc";
|
||||||
|
|
||||||
this.state.currentSort = { column, direction: newDirection };
|
this.state.currentSort = { column, direction };
|
||||||
|
|
||||||
this.elements.container.querySelectorAll('th.sortable').forEach(th => {
|
// Update UI
|
||||||
th.setAttribute('aria-sort', 'none');
|
this.elements.container.querySelectorAll("th.sortable").forEach(header => {
|
||||||
th.classList.remove('sorted-asc', 'sorted-desc');
|
header.setAttribute("aria-sort", "none");
|
||||||
|
header.classList.remove("sorted-asc", "sorted-desc");
|
||||||
});
|
});
|
||||||
|
|
||||||
header.setAttribute('aria-sort', newDirection === 'asc' ? 'ascending' : 'descending');
|
sortableHeader.setAttribute("aria-sort", direction === "asc" ? "ascending" : "descending");
|
||||||
header.classList.add(`sorted-${newDirection}`);
|
sortableHeader.classList.add(`sorted-${direction}`);
|
||||||
|
|
||||||
this._sortData();
|
this._sortData();
|
||||||
this._updateTableContent();
|
this._updateTableContent();
|
||||||
|
|
@ -348,23 +393,36 @@ class TableManager {
|
||||||
const entries = Object.entries(this.state.filteredData);
|
const entries = Object.entries(this.state.filteredData);
|
||||||
|
|
||||||
entries.sort(([hostA, dataA], [hostB, dataB]) => {
|
entries.sort(([hostA, dataA], [hostB, dataB]) => {
|
||||||
const valueA = column === 'service' ? hostA.toLowerCase() : (dataA[column] === 'yes' ? 1 : 0);
|
let valueA, valueB;
|
||||||
const valueB = column === 'service' ? hostB.toLowerCase() : (dataB[column] === 'yes' ? 1 : 0);
|
|
||||||
|
|
||||||
const comparison = valueA > valueB ? 1 : valueA < valueB ? -1 : 0;
|
if (column === "service") {
|
||||||
return direction === 'asc' ? comparison : -comparison;
|
valueA = hostA.toLowerCase();
|
||||||
|
valueB = hostB.toLowerCase();
|
||||||
|
} else {
|
||||||
|
// Convert "yes"/"no" to numeric values for sorting
|
||||||
|
valueA = dataA[column] === "yes" ? 1 : 0;
|
||||||
|
valueB = dataB[column] === "yes" ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let comparison = 0;
|
||||||
|
if (valueA > valueB) comparison = 1;
|
||||||
|
else if (valueA < valueB) comparison = -1;
|
||||||
|
|
||||||
|
return direction === "asc" ? comparison : -comparison;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.state.filteredData = Object.fromEntries(entries);
|
this.state.filteredData = Object.fromEntries(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateTableContent() {
|
_updateTableContent() {
|
||||||
const tbody = this.elements.container.querySelector('tbody');
|
const tbody = this.elements.container.querySelector("tbody");
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
const firstKey = Object.keys(this.state.currentData)[0];
|
|
||||||
const providers = firstKey ? Object.keys(this.state.currentData[firstKey]) : [];
|
const firstHostKey = Object.keys(this.state.currentData)[0];
|
||||||
|
const columns = firstHostKey ? Object.keys(this.state.currentData[firstHostKey]) : [];
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
tbody.innerHTML = this._generateTableRows(this.state.filteredData, providers);
|
tbody.innerHTML = this._generateTableRows(this.state.filteredData, columns);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -379,69 +437,109 @@ class ComparisonManager {
|
||||||
select1: document.getElementById(select1Id),
|
select1: document.getElementById(select1Id),
|
||||||
select2: document.getElementById(select2Id)
|
select2: document.getElementById(select2Id)
|
||||||
};
|
};
|
||||||
|
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.isComparing = false;
|
this.isComparing = false;
|
||||||
|
|
||||||
this.handleCompare = this._generateCompareTable.bind(this);
|
this.handleCompare = this._generateCompareTable.bind(this);
|
||||||
|
this.services = []; // Store services array for indexed format
|
||||||
|
|
||||||
|
// Check if data is in indexed format
|
||||||
|
if (data.services && data.supported) {
|
||||||
|
this.services = data.services;
|
||||||
|
this.isIndexedFormat = true;
|
||||||
|
} else {
|
||||||
|
// Extract services from regular format
|
||||||
|
const firstHost = Object.values(data)[0];
|
||||||
|
this.services = firstHost ? Object.keys(firstHost) : [];
|
||||||
|
this.isIndexedFormat = false;
|
||||||
|
}
|
||||||
|
|
||||||
this._init();
|
this._init();
|
||||||
}
|
}
|
||||||
|
|
||||||
_init() {
|
_init() {
|
||||||
this._populateDropdowns();
|
this._populateDropdowns();
|
||||||
|
|
||||||
this.elements.select1?.addEventListener('change', this.handleCompare);
|
if (this.elements.select1) {
|
||||||
this.elements.select2?.addEventListener('change', this.handleCompare);
|
this.elements.select1.addEventListener("change", this.handleCompare);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', e => {
|
if (this.elements.select2) {
|
||||||
if (this.isComparing && e.key === 'Escape') this._closeComparison();
|
this.elements.select2.addEventListener("change", this.handleCompare);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (this.isComparing && e.key === "Escape") {
|
||||||
|
this._closeComparison();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_populateDropdowns() {
|
_populateDropdowns() {
|
||||||
const firstKey = Object.keys(this.data)[0];
|
// Use the stored services array
|
||||||
const providers = firstKey ? Object.keys(this.data[firstKey]) : [];
|
const optionsHtml = '<option value="">Choose a service...</option>' +
|
||||||
const optionsHTML = '<option value="">Choose a service...</option>' +
|
this.services.map(service =>
|
||||||
providers.map(p => `<option value="${p}">${p}</option>`).join('');
|
`<option value="${service}">${service}</option>`
|
||||||
|
).join("");
|
||||||
|
|
||||||
[this.elements.select1, this.elements.select2].forEach(select => {
|
[this.elements.select1, this.elements.select2].forEach(select => {
|
||||||
if (select) select.innerHTML = optionsHTML;
|
if (select) {
|
||||||
|
select.innerHTML = optionsHtml;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_generateCompareTable() {
|
// Transform indexed data to regular format for comparison
|
||||||
const provider1 = this.elements.select1?.value;
|
_getHostData(hostName) {
|
||||||
const provider2 = this.elements.select2?.value;
|
if (this.isIndexedFormat) {
|
||||||
|
// Convert indexed format to regular format
|
||||||
|
const supportedIndices = this.data.supported[hostName] || [];
|
||||||
|
const hostData = {};
|
||||||
|
this.services.forEach((service, index) => {
|
||||||
|
hostData[service] = supportedIndices.includes(index) ? "yes" : "no";
|
||||||
|
});
|
||||||
|
return hostData;
|
||||||
|
} else {
|
||||||
|
// Return data as-is for regular format
|
||||||
|
return this.data[hostName] || {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!provider1 || !provider2) {
|
_generateCompareTable() {
|
||||||
|
const service1 = this.elements.select1?.value;
|
||||||
|
const service2 = this.elements.select2?.value;
|
||||||
|
|
||||||
|
if (!service1 || !service2) {
|
||||||
this._showEmptyState();
|
this._showEmptyState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (provider1 === provider2) {
|
|
||||||
|
if (service1 === service2) {
|
||||||
this._showSameProviderWarning();
|
this._showSameProviderWarning();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isComparing = true;
|
this.isComparing = true;
|
||||||
const loaderId = loadingManager.show(this.elements.container, 'Generating comparison...');
|
const loaderId = loadingManager.show(this.elements.container, "Generating comparison...");
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this._renderComparisonTable(provider1, provider2);
|
this._renderComparisonTable(service1, service2);
|
||||||
loadingManager.hide(this.elements.container, loaderId);
|
loadingManager.hide(this.elements.container, loaderId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderComparisonTable(provider1, provider2) {
|
_renderComparisonTable(service1, service2) {
|
||||||
const stats = this._calculateComparisonStats(provider1, provider2);
|
const stats = this._calculateComparisonStats(service1, service2);
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const wrapper = document.createElement('div');
|
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
wrapper.innerHTML = `
|
wrapper.innerHTML = `
|
||||||
<div class="comparison-header">
|
<div class="comparison-header">
|
||||||
<h3>Comparing ${provider1} vs ${provider2}</h3>
|
<h3>Comparing ${service1} vs ${service2}</h3>
|
||||||
<div class="comparison-stats">
|
<div class="comparison-stats">
|
||||||
<div class="stat"><span class="stat-label">Shared Support</span><span class="stat-value">${stats.shared}</span></div>
|
<div class="stat"><span class="stat-label">Shared Support</span><span class="stat-value">${stats.shared}</span></div>
|
||||||
<div class="stat"><span class="stat-label">${provider1} Only</span><span class="stat-value">${stats.provider1Only}</span></div>
|
<div class="stat"><span class="stat-label">${service1} Only</span><span class="stat-value">${stats.service1Only}</span></div>
|
||||||
<div class="stat"><span class="stat-label">${provider2} Only</span><span class="stat-value">${stats.provider2Only}</span></div>
|
<div class="stat"><span class="stat-label">${service2} Only</span><span class="stat-value">${stats.service2Only}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="comparison-actions">
|
<div class="comparison-actions">
|
||||||
<button id="close-compare" class="btn btn-secondary">
|
<button id="close-compare" class="btn btn-secondary">
|
||||||
|
|
@ -473,111 +571,147 @@ class ComparisonManager {
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Service Name</th>
|
<th>Service Name</th>
|
||||||
<th class="provider-header ${provider1.toLowerCase()}">${provider1}</th>
|
<th class="provider-header ${service1.toLowerCase().replace(/\s+/g, '-')}">${service1}</th>
|
||||||
<th class="provider-header ${provider2.toLowerCase()}">${provider2}</th>
|
<th class="provider-header ${service2.toLowerCase().replace(/\s+/g, '-')}">${service2}</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${this._generateComparisonRows(provider1, provider2)}
|
${this._generateComparisonRows(service1, service2)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
fragment.appendChild(wrapper);
|
fragment.appendChild(wrapper);
|
||||||
this.elements.container.innerHTML = '';
|
this.elements.container.innerHTML = "";
|
||||||
this.elements.container.appendChild(fragment);
|
this.elements.container.appendChild(fragment);
|
||||||
this.elements.container.style.display = 'block';
|
this.elements.container.style.display = "block";
|
||||||
|
|
||||||
this._attachComparisonEvents();
|
this._attachComparisonEvents();
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.elements.container.classList.add('comparison-visible');
|
this.elements.container.classList.add("comparison-visible");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_generateComparisonRows(provider1, provider2) {
|
_generateComparisonRows(service1, service2) {
|
||||||
const rows = [];
|
const rows = [];
|
||||||
const checkSvg = '<svg class="table-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><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></svg>';
|
const checkIcon = '<svg class="table-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><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></svg>';
|
||||||
const crossSvg = '<svg class="table-cross" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><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></svg>';
|
const crossIcon = '<svg class="table-cross" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><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></svg>';
|
||||||
|
|
||||||
for (const [host, providerData] of Object.entries(this.data)) {
|
// Get all hosts (keys from the original data structure)
|
||||||
const support1 = providerData[provider1] === 'yes';
|
const hosts = this.isIndexedFormat ? Object.keys(this.data.supported) : Object.keys(this.data);
|
||||||
const support2 = providerData[provider2] === 'yes';
|
|
||||||
|
|
||||||
let statusClass = 'neither-supported';
|
for (const host of hosts) {
|
||||||
let statusText = 'Neither';
|
const hostData = this._getHostData(host);
|
||||||
if (support1 && support2) { statusClass = 'both-supported'; statusText = 'Both'; }
|
const supported1 = hostData[service1] === "yes";
|
||||||
else if (support1) { statusClass = 'provider1-only'; statusText = `${provider1} only`; }
|
const supported2 = hostData[service2] === "yes";
|
||||||
else if (support2) { statusClass = 'provider2-only'; statusText = `${provider2} only`; }
|
|
||||||
|
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`;
|
||||||
|
}
|
||||||
|
|
||||||
rows.push(`
|
rows.push(`
|
||||||
<tr class="comparison-row ${statusClass}" data-status="${statusClass}">
|
<tr class="comparison-row ${statusClass}" data-status="${statusClass}">
|
||||||
<td class="service-name">${host}</td>
|
<td class="service-name">${host}</td>
|
||||||
<td class="support-status ${support1 ? 'supported' : 'not-supported'}">
|
<td class="support-status ${supported1 ? "supported" : "not-supported"}">
|
||||||
<span class="status-indicator" aria-label="${support1 ? 'Supported' : 'Not supported'}">${support1 ? checkSvg : crossSvg}</span>
|
<span class="status-indicator" aria-label="${supported1 ? "Supported" : "Not supported"}">
|
||||||
|
${supported1 ? checkIcon : crossIcon}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="support-status ${support2 ? 'supported' : 'not-supported'}">
|
<td class="support-status ${supported2 ? "supported" : "not-supported"}">
|
||||||
<span class="status-indicator" aria-label="${support2 ? 'Supported' : 'Not supported'}">${support2 ? checkSvg : crossSvg}</span>
|
<span class="status-indicator" aria-label="${supported2 ? "Supported" : "Not supported"}">
|
||||||
|
${supported2 ? checkIcon : crossIcon}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="status-text"><span class="status-badge ${statusClass}">${statusText}</span></td>
|
<td class="status-text"><span class="status-badge ${statusClass}">${statusText}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
return rows.join('');
|
|
||||||
|
return rows.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
_calculateComparisonStats(provider1, provider2) {
|
_calculateComparisonStats(service1, service2) {
|
||||||
let shared = 0, provider1Only = 0, provider2Only = 0;
|
let shared = 0;
|
||||||
for (const providerData of Object.values(this.data)) {
|
let service1Only = 0;
|
||||||
const s1 = providerData[provider1] === 'yes';
|
let service2Only = 0;
|
||||||
const s2 = providerData[provider2] === 'yes';
|
|
||||||
if (s1 && s2) shared++;
|
// Get all hosts
|
||||||
else if (s1) provider1Only++;
|
const hosts = this.isIndexedFormat ? Object.keys(this.data.supported) : Object.keys(this.data);
|
||||||
else if (s2) provider2Only++;
|
|
||||||
|
for (const host of hosts) {
|
||||||
|
const hostData = this._getHostData(host);
|
||||||
|
const supported1 = hostData[service1] === "yes";
|
||||||
|
const supported2 = hostData[service2] === "yes";
|
||||||
|
|
||||||
|
if (supported1 && supported2) {
|
||||||
|
shared++;
|
||||||
|
} else if (supported1) {
|
||||||
|
service1Only++;
|
||||||
|
} else if (supported2) {
|
||||||
|
service2Only++;
|
||||||
}
|
}
|
||||||
return { shared, provider1Only, provider2Only };
|
}
|
||||||
|
|
||||||
|
return { shared, service1Only, service2Only };
|
||||||
}
|
}
|
||||||
|
|
||||||
_attachComparisonEvents() {
|
_attachComparisonEvents() {
|
||||||
const closeBtn = this.elements.container.querySelector('#close-compare');
|
const closeBtn = this.elements.container.querySelector("#close-compare");
|
||||||
closeBtn?.addEventListener('click', () => this._closeComparison());
|
if (closeBtn) {
|
||||||
|
closeBtn.addEventListener("click", () => this._closeComparison());
|
||||||
|
}
|
||||||
|
|
||||||
this.elements.container.addEventListener('change', e => {
|
this.elements.container.addEventListener("change", (e) => {
|
||||||
if (e.target.name === 'comparison-filter') {
|
if (e.target.name === "comparison-filter") {
|
||||||
this._filterComparison(e.target.value);
|
this._filterComparison(e.target.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_filterComparison(filter) {
|
_filterComparison(filterValue) {
|
||||||
const rows = this.elements.container.querySelectorAll('.comparison-row');
|
const rows = this.elements.container.querySelectorAll(".comparison-row");
|
||||||
let visibleCount = 0;
|
let visibleCount = 0;
|
||||||
|
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
const status = row.dataset.status;
|
const status = row.dataset.status;
|
||||||
const show = filter === 'all' ||
|
const shouldShow =
|
||||||
(filter === 'both' && status === 'both-supported') ||
|
filterValue === "all" ||
|
||||||
(filter === 'different' && (status === 'provider1-only' || status === 'provider2-only'));
|
(filterValue === "both" && status === "both-supported") ||
|
||||||
row.style.display = show ? '' : 'none';
|
(filterValue === "different" && (status === "service1-only" || status === "service2-only"));
|
||||||
if (show) visibleCount++;
|
|
||||||
|
row.style.display = shouldShow ? "" : "none";
|
||||||
|
if (shouldShow) visibleCount++;
|
||||||
});
|
});
|
||||||
|
|
||||||
let indicator = this.elements.container.querySelector('.filter-results');
|
let resultsElement = this.elements.container.querySelector(".filter-results");
|
||||||
if (!indicator) {
|
if (!resultsElement) {
|
||||||
indicator = document.createElement('div');
|
resultsElement = document.createElement("div");
|
||||||
indicator.className = 'filter-results';
|
resultsElement.className = "filter-results";
|
||||||
const tableWrapper = this.elements.container.querySelector('.table-wrapper');
|
const tableWrapper = this.elements.container.querySelector(".table-wrapper");
|
||||||
tableWrapper.parentNode.insertBefore(indicator, tableWrapper);
|
tableWrapper.parentNode.insertBefore(resultsElement, tableWrapper);
|
||||||
}
|
}
|
||||||
indicator.textContent = `Showing ${visibleCount} services`;
|
|
||||||
|
resultsElement.textContent = `Showing ${visibleCount} services`;
|
||||||
}
|
}
|
||||||
|
|
||||||
_closeComparison() {
|
_closeComparison() {
|
||||||
this.elements.container.classList.add('comparison-hiding');
|
this.elements.container.classList.add("comparison-hiding");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.elements.container.style.display = 'none';
|
this.elements.container.style.display = "none";
|
||||||
this.elements.container.classList.remove('comparison-visible', 'comparison-hiding');
|
this.elements.container.classList.remove("comparison-visible", "comparison-hiding");
|
||||||
this.isComparing = false;
|
this.isComparing = false;
|
||||||
this._showEmptyState();
|
this._showEmptyState();
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
@ -591,7 +725,7 @@ class ComparisonManager {
|
||||||
<p>Select two services above to see a detailed comparison</p>
|
<p>Select two services above to see a detailed comparison</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
this.elements.container.style.display = 'block';
|
this.elements.container.style.display = "block";
|
||||||
}
|
}
|
||||||
|
|
||||||
_showSameProviderWarning() {
|
_showSameProviderWarning() {
|
||||||
|
|
@ -602,10 +736,9 @@ class ComparisonManager {
|
||||||
<p>Please select two different services to compare</p>
|
<p>Please select two different services to compare</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
this.elements.container.style.display = 'block';
|
this.elements.container.style.display = "block";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==============================
|
/* ==============================
|
||||||
Pricing Manager
|
Pricing Manager
|
||||||
============================== */
|
============================== */
|
||||||
|
|
@ -936,7 +1069,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
loadingManager.show('#pricing-table-container', 'Loading pricing data...')
|
loadingManager.show('#pricing-table-container', 'Loading pricing data...')
|
||||||
];
|
];
|
||||||
|
|
||||||
const dataUrls = ['./json/file-hosts.json', './json/adult-hosts.json', './json/pricing.json'];
|
const dataUrls = ['./json/file-hosts-optimized.json', './json/adult-hosts-optimized.json', './json/pricing.json'];
|
||||||
const fetchPromises = dataUrls.map(url =>
|
const fetchPromises = dataUrls.map(url =>
|
||||||
fetch(url).then(r => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status} for ${url}`)))
|
fetch(url).then(r => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status} for ${url}`)))
|
||||||
);
|
);
|
||||||
|
|
@ -966,7 +1099,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error loading data (${dataUrls[index]}):`, result.reason);
|
console.error(`Error loading data (${dataUrls[index]}):`, result.reason);
|
||||||
const el = document.querySelector(containers[index]);
|
const el = document.querySelector(containers[index]);
|
||||||
if (el) el.innerHTML = `<div class="error-state"><p>Failed to load data: ${dataUrls[index]}</p></div>`;
|
if (el) el.innerHTML = `<div class="error-state"><p>Failed to load data: <span class="error-dataurl">${dataUrls[index]}</span></p></div>`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
12
dist/scripts/replace-assets.js
vendored
12
dist/scripts/replace-assets.js
vendored
|
|
@ -1,12 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const file = path.resolve(__dirname, '..', 'dist', 'index.html');
|
|
||||||
let html = fs.readFileSync(file, 'utf8');
|
|
||||||
|
|
||||||
html = html
|
|
||||||
.replace(/href=["']\.\/css\/styles\.css["']/g, 'href="./css/styles-min.css"')
|
|
||||||
.replace(/src=["']js\/app\.js["']/g, 'src="js/app-min.js"');
|
|
||||||
|
|
||||||
fs.writeFileSync(file, html, 'utf8');
|
|
||||||
console.log('Replaced asset links in dist/index.html');
|
|
||||||
|
|
@ -11,7 +11,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"minify:css": "cleancss -o dist/css/styles-min.css dist/css/styles.css",
|
"minify:css": "cleancss -o dist/css/styles-min.css dist/css/styles.css",
|
||||||
"minify:js": "terser dist/js/app.js -o dist/js/app-min.js --compress --mangle",
|
"minify:js": "terser dist/js/app.js -o dist/js/app-min.js --compress --mangle",
|
||||||
"build": "npm run minify:css && npm run minify:js && workbox generateSW workbox-config.js",
|
"optimize-json": "chmod +x scripts/json-optimizer.sh && scripts/json-optimizer.sh dist/json/file-hosts.json && scripts/json-optimizer.sh dist/json/adult-hosts.json",
|
||||||
|
"build": "npm run minify:css && npm run minify:js && npm run optimize-json && workbox generateSW workbox-config.js",
|
||||||
"postbuild": "node scripts/replace-assets.js"
|
"postbuild": "node scripts/replace-assets.js"
|
||||||
},
|
},
|
||||||
"author": "fynks",
|
"author": "fynks",
|
||||||
|
|
|
||||||
191
scripts/json-optimizer.sh
Executable file
191
scripts/json-optimizer.sh
Executable file
|
|
@ -0,0 +1,191 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# JSON Optimizer Script - Converts host data to indexed format
|
||||||
|
# Usage: ./scripts/json-optimizer.sh file-hosts.json
|
||||||
|
# ./scripts/json-optimizer.sh adult-hosts.json
|
||||||
|
# ./scripts/json-optimizer.sh pricing.json
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Function to print colored output
|
||||||
|
print_status() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DEFAULT_INPUT_DIR="."
|
||||||
|
DEFAULT_OUTPUT_DIR="."
|
||||||
|
|
||||||
|
# Check if at least one argument is provided
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
print_error "No input file specified"
|
||||||
|
echo "Usage: $0 <input-file.json> [output-file.json]"
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 file-hosts.json"
|
||||||
|
echo " $0 ./json/adult-hosts.json"
|
||||||
|
echo " $0 pricing.json pricing-optimized.json"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get input file
|
||||||
|
INPUT_FILE="$1"
|
||||||
|
|
||||||
|
# Generate output filename if not provided
|
||||||
|
if [ $# -eq 2 ]; then
|
||||||
|
OUTPUT_FILE="$2"
|
||||||
|
else
|
||||||
|
# Extract filename without path and extension
|
||||||
|
FILENAME=$(basename "$INPUT_FILE")
|
||||||
|
NAME="${FILENAME%.*}"
|
||||||
|
EXT="${FILENAME##*.}"
|
||||||
|
OUTPUT_FILE="${NAME}-optimized.${EXT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if input file exists
|
||||||
|
if [ ! -f "$INPUT_FILE" ]; then
|
||||||
|
print_error "Input file '$INPUT_FILE' not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create output directory if it doesn't exist
|
||||||
|
OUTPUT_DIR=$(dirname "$OUTPUT_FILE")
|
||||||
|
if [ ! -d "$OUTPUT_DIR" ] && [ "$OUTPUT_DIR" != "." ]; then
|
||||||
|
print_status "Creating output directory: $OUTPUT_DIR"
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_status "Processing: $INPUT_FILE"
|
||||||
|
print_status "Output will be saved to: $OUTPUT_FILE"
|
||||||
|
|
||||||
|
# Python script to handle the conversion
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
def convert_to_indexed_format(input_file, output_file):
|
||||||
|
try:
|
||||||
|
# Read the input JSON
|
||||||
|
print('Reading input file...')
|
||||||
|
with open(input_file, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
host_count = len(data)
|
||||||
|
print(f'Loaded {host_count:,} hosts')
|
||||||
|
|
||||||
|
# Handle empty data
|
||||||
|
if not data:
|
||||||
|
print('Warning: Input file is empty')
|
||||||
|
output_data = {'services': [], 'supported': {}}
|
||||||
|
with open(output_file, 'w') as f:
|
||||||
|
json.dump(output_data, f, separators=(',', ':'))
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Extract all services (keys from entries)
|
||||||
|
all_services = set()
|
||||||
|
for host_data in data.values():
|
||||||
|
all_services.update(host_data.keys())
|
||||||
|
|
||||||
|
services = sorted(list(all_services))
|
||||||
|
service_count = len(services)
|
||||||
|
print(f'Found {service_count} services: {\", \".join(services[:5])}{\"...\" if service_count > 5 else \"\"}')
|
||||||
|
|
||||||
|
# Create service to index mapping
|
||||||
|
service_to_index = {service: idx for idx, service in enumerate(services)}
|
||||||
|
|
||||||
|
# Convert data to indexed format
|
||||||
|
print('Converting data to indexed format...')
|
||||||
|
supported = {}
|
||||||
|
total_support_instances = 0
|
||||||
|
|
||||||
|
for host, host_data in data.items():
|
||||||
|
# Get indices of services with 'yes' values (case insensitive)
|
||||||
|
supported_indices = [
|
||||||
|
service_to_index[service]
|
||||||
|
for service, value in host_data.items()
|
||||||
|
if str(value).lower() == 'yes'
|
||||||
|
]
|
||||||
|
supported[host] = sorted(supported_indices)
|
||||||
|
total_support_instances += len(supported_indices)
|
||||||
|
|
||||||
|
# Create output structure
|
||||||
|
output_data = {
|
||||||
|
'services': services,
|
||||||
|
'supported': supported
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write optimized JSON (compact format)
|
||||||
|
print('Writing optimized JSON...')
|
||||||
|
with open(output_file, 'w') as f:
|
||||||
|
json.dump(output_data, f, separators=(',', ':'))
|
||||||
|
|
||||||
|
# Get file sizes for comparison
|
||||||
|
original_size = os.path.getsize(input_file)
|
||||||
|
new_size = os.path.getsize(output_file)
|
||||||
|
reduction = ((original_size - new_size) / original_size) * 100 if original_size > 0 else 0
|
||||||
|
|
||||||
|
print('✅ Conversion successful!')
|
||||||
|
print(f'📁 Original size: {original_size:,} bytes')
|
||||||
|
print(f'📁 New size: {new_size:,} bytes')
|
||||||
|
print(f'📉 Size reduction: {reduction:.1f}%')
|
||||||
|
print(f'💾 Output saved to: {output_file}')
|
||||||
|
|
||||||
|
# Show statistics
|
||||||
|
avg_support_per_host = total_support_instances / host_count if host_count > 0 else 0
|
||||||
|
|
||||||
|
print('📈 Statistics:')
|
||||||
|
print(f' - Hosts: {host_count:,}')
|
||||||
|
print(f' - Services: {service_count:,}')
|
||||||
|
print(f' - Total support instances: {total_support_instances:,}')
|
||||||
|
print(f' - Average support per host: {avg_support_per_host:.1f}')
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f'❌ JSON parsing error: {str(e)}')
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f'❌ Error during conversion: {str(e)}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Run the conversion
|
||||||
|
success = convert_to_indexed_format('$INPUT_FILE', '$OUTPUT_FILE')
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
sys.exit(1)
|
||||||
|
" || {
|
||||||
|
print_error "Python conversion failed. Please ensure Python 3 is installed."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success "Conversion completed successfully!"
|
||||||
|
|
||||||
|
# Optional: Show file size comparison in bash as well
|
||||||
|
if command -v stat >/dev/null 2>&1; then
|
||||||
|
ORIGINAL_SIZE=$(stat -f%z "$INPUT_FILE" 2>/dev/null || stat -c%s "$INPUT_FILE" 2>/dev/null)
|
||||||
|
NEW_SIZE=$(stat -f%z "$OUTPUT_FILE" 2>/dev/null || stat -c%s "$OUTPUT_FILE" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ $? -eq 0 ] && [ $ORIGINAL_SIZE -gt 0 ]; then
|
||||||
|
REDUCTION_PERCENTAGE=$(( (ORIGINAL_SIZE - NEW_SIZE) * 100 / ORIGINAL_SIZE ))
|
||||||
|
print_status "Final size reduction: ${REDUCTION_PERCENTAGE}%"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
Loading…
Reference in a new issue