From be135cb02aefab826001ed13c59c32aaddb69af9 Mon Sep 17 00:00:00 2001
From: fynks <75840152+fynks@users.noreply.github.com>
Date: Mon, 25 Aug 2025 10:16:57 +0500
Subject: [PATCH] optimize json
---
dist/css/styles.css | 11 +
dist/index.html | 6 +-
dist/js/app-min.js | 1 +
dist/js/app.js | 1223 ++++++++++++++++++--------------
dist/scripts/replace-assets.js | 12 -
package.json | 3 +-
scripts/json-optimizer.sh | 191 +++++
7 files changed, 886 insertions(+), 561 deletions(-)
create mode 100644 dist/js/app-min.js
delete mode 100644 dist/scripts/replace-assets.js
create mode 100755 scripts/json-optimizer.sh
diff --git a/dist/css/styles.css b/dist/css/styles.css
index da4509f..7679277 100644
--- a/dist/css/styles.css
+++ b/dist/css/styles.css
@@ -1409,6 +1409,17 @@ select:focus {
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 {
display: flex;
flex-direction: column;
diff --git a/dist/index.html b/dist/index.html
index cdd352f..479193b 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -40,7 +40,7 @@
-
+
+
diff --git a/dist/js/app-min.js b/dist/js/app-min.js
new file mode 100644
index 0000000..6ca0b42
--- /dev/null
+++ b/dist/js/app-min.js
@@ -0,0 +1 @@
+const Utils=(()=>{const e=new WeakMap;return{debounce(t,n=300,s=!1){if(e.has(t))return e.get(t);let a;const r=function(...e){const r=s&&!a;clearTimeout(a),r&&t.apply(this,e),a=setTimeout(()=>{a=null,s||t.apply(this,e)},n)};return e.set(t,r),r},throttle(e,t=100){let n=!1;return function(...s){n||(e.apply(this,s),n=!0,requestAnimationFrame(()=>{setTimeout(()=>n=!1,t)}))}},animateOnScroll:(()=>{let e;return(t,n={})=>{"IntersectionObserver"in window&&(e||(e=new IntersectionObserver(t=>{t.forEach(t=>{t.isIntersecting&&(t.target.classList.add("animate-in"),e.unobserve(t.target))})},{threshold:.1,rootMargin:"0px 0px -50px 0px",...n})),t.forEach(t=>e.observe(t)))}})()}})();class LoadingManager{constructor(){this.activeLoaders=new Map,this.template=this.createTemplate()}createTemplate(){const e=document.createElement("template");return e.innerHTML='\n
\n ',e}show(e,t="Loading..."){const n="string"==typeof e?document.querySelector(e):e;if(!n)return null;const s=`loader-${Date.now()}-${Math.random().toString(36).slice(2)}`,a=this.template.content.cloneNode(!0).firstElementChild;a.dataset.loaderId=s,a.querySelector(".loading-text").textContent=t;return"static"===getComputedStyle(n).position&&(n.style.position="relative"),n.appendChild(a),this.activeLoaders.set(s,a),requestAnimationFrame(()=>a.classList.add("loading-overlay--visible")),s}hide(e,t){const n=this.activeLoaders.get(t);n&&(n.classList.add("loading-overlay--hiding"),this.activeLoaders.delete(t),setTimeout(()=>n.remove(),250))}hideAll(){this.activeLoaders.forEach((e,t)=>this.hide(null,t))}}class TableManager{constructor(e,t,n,s={}){this.elements={container:document.getElementById(e),searchInput:document.getElementById(t),clearIcon:document.getElementById(n)},this.options={sortable:!0,filterable:!0,pagination:!1,itemsPerPage:50,...s},this.state={currentData:{},filteredData:{},currentSort:{column:null,direction:"asc"},currentPage:1,services:[],isIndexedFormat:!1},this.handleSearch=Utils.debounce(this._performSearch.bind(this),250),this.handleSort=this._performSort.bind(this),this._init()}_init(){this.elements.searchInput&&(this.elements.searchInput.addEventListener("input",this.handleSearch),this.elements.searchInput.addEventListener("keydown",e=>{"Escape"===e.key&&this._clearSearch()})),this.elements.clearIcon&&this.elements.clearIcon.addEventListener("click",()=>this._clearSearch())}_transformIndexedData(e){const{services:t,supported:n}=e,s={};for(const[e,a]of Object.entries(n))s[e]={},t.forEach((t,n)=>{s[e][t]=a.includes(n)?"yes":"no"});return this.state.services=t,this.state.isIndexedFormat=!0,s}generateTable(e={}){if(!this.elements.container)return;let t;if(e.services&&e.supported)t=this._transformIndexedData(e);else{t=e,this.state.isIndexedFormat=!1;const n=Object.values(t)[0];this.state.services=n?Object.keys(n):[]}if(!Object.keys(t).length)return void(this.elements.container.innerHTML='');this.state.currentData=t,this.state.filteredData={...t};const n=Object.keys(t)[0],s=n?Object.keys(t[n]):[],a=this.elements.container.id.replace("-container",""),r=document.createDocumentFragment(),i=document.createElement("div");i.className="table-wrapper";const o=document.createElement("table");o.id=a,o.className="enhanced-table",o.setAttribute("aria-label","Service Comparison Table");const l=document.createElement("thead");l.innerHTML=this._generateTableHeader(s),o.appendChild(l);const c=document.createElement("tbody");c.innerHTML=this._generateTableRows(this.state.filteredData,s),o.appendChild(c),i.appendChild(o),r.appendChild(i),this.elements.container.innerHTML="",this.elements.container.appendChild(r),this._attachTableEvents()}_generateTableHeader(e){return`\n \n \n Service Name \n \n \n \n \n ${e.map(e=>`\n \n ${e} \n \n \n \n \n `).join("")}\n \n `}_generateTableRows(e,t){const n=[];for(const[s,a]of Object.entries(e))n.push(`\n \n \n \n ${s} \n
\n \n ${t.map(e=>{const t="yes"===a[e];return`\n \n \n ${t?' ':' '}\n \n \n `}).join("")}\n \n `);return n.join("")}_attachTableEvents(){if(!this.options.sortable)return;const e=this.elements.container.querySelector("thead");e&&(e.addEventListener("click",this.handleSort),e.addEventListener("keydown",e=>{const t=e.target.closest(".sortable");!t||"Enter"!==e.key&&" "!==e.key||(e.preventDefault(),this.handleSort({target:t}))}))}_performSearch(){const e=(this.elements.searchInput?.value||"").toLowerCase().trim();e?(this.state.filteredData=Object.fromEntries(Object.entries(this.state.currentData).filter(([t])=>t.toLowerCase().includes(e))),this.elements.clearIcon&&(this.elements.clearIcon.style.display="block")):(this.state.filteredData={...this.state.currentData},this.elements.clearIcon&&(this.elements.clearIcon.style.display="none")),this._updateTableContent(),this._updateSearchResults(e)}_clearSearch(){this.elements.searchInput&&(this.elements.searchInput.value=""),this.state.filteredData={...this.state.currentData},this.elements.clearIcon&&(this.elements.clearIcon.style.display="none"),this._updateTableContent(),this._updateSearchResults("")}_updateSearchResults(e){if(!this.elements.container)return;let t=this.elements.container.querySelector(".search-results");if(e){const e=Object.keys(this.state.filteredData).length,n=Object.keys(this.state.currentData).length;t||(t=document.createElement("div"),t.className="search-results",this.elements.container.insertBefore(t,this.elements.container.firstChild)),t.textContent=`Showing ${e} of ${n} services`,t.style.display="block",t.style.textAlign="center",t.style.padding="0.5em 0"}else t&&(t.style.display="none")}_performSort(e){const t=e.target.closest(".sortable");if(!t)return;const n=t.dataset.column,{currentSort:s}=this.state,a=s.column===n&&"asc"===s.direction?"desc":"asc";this.state.currentSort={column:n,direction:a},this.elements.container.querySelectorAll("th.sortable").forEach(e=>{e.setAttribute("aria-sort","none"),e.classList.remove("sorted-asc","sorted-desc")}),t.setAttribute("aria-sort","asc"===a?"ascending":"descending"),t.classList.add(`sorted-${a}`),this._sortData(),this._updateTableContent()}_sortData(){const{column:e,direction:t}=this.state.currentSort,n=Object.entries(this.state.filteredData);n.sort(([n,s],[a,r])=>{let i,o;"service"===e?(i=n.toLowerCase(),o=a.toLowerCase()):(i="yes"===s[e]?1:0,o="yes"===r[e]?1:0);let l=0;return i>o?l=1:i{e.innerHTML=this._generateTableRows(this.state.filteredData,n)})}}class ComparisonManager{constructor(e,t,n,s){if(this.elements={container:document.getElementById(e),select1:document.getElementById(t),select2:document.getElementById(n)},this.data=s,this.isComparing=!1,this.handleCompare=this._generateCompareTable.bind(this),this.services=[],s.services&&s.supported)this.services=s.services,this.isIndexedFormat=!0;else{const e=Object.values(s)[0];this.services=e?Object.keys(e):[],this.isIndexedFormat=!1}this._init()}_init(){this._populateDropdowns(),this.elements.select1&&this.elements.select1.addEventListener("change",this.handleCompare),this.elements.select2&&this.elements.select2.addEventListener("change",this.handleCompare),document.addEventListener("keydown",e=>{this.isComparing&&"Escape"===e.key&&this._closeComparison()})}_populateDropdowns(){const e='Choose a service... '+this.services.map(e=>`${e} `).join("");[this.elements.select1,this.elements.select2].forEach(t=>{t&&(t.innerHTML=e)})}_getHostData(e){if(this.isIndexedFormat){const t=this.data.supported[e]||[],n={};return this.services.forEach((e,s)=>{n[e]=t.includes(s)?"yes":"no"}),n}return this.data[e]||{}}_generateCompareTable(){const e=this.elements.select1?.value,t=this.elements.select2?.value;if(!e||!t)return void this._showEmptyState();if(e===t)return void this._showSameProviderWarning();this.isComparing=!0;const n=loadingManager.show(this.elements.container,"Generating comparison...");requestAnimationFrame(()=>{this._renderComparisonTable(e,t),loadingManager.hide(this.elements.container,n)})}_renderComparisonTable(e,t){const n=this._calculateComparisonStats(e,t),s=document.createDocumentFragment(),a=document.createElement("div");a.innerHTML=`\n \n\n \n \n \n All Services \n \n \n \n Supported by Both \n \n \n \n Different Support \n \n
\n\n \n
\n \n \n Service Name \n \n \n Status \n \n \n \n ${this._generateComparisonRows(e,t)}\n \n
\n
\n `,s.appendChild(a),this.elements.container.innerHTML="",this.elements.container.appendChild(s),this.elements.container.style.display="block",this._attachComparisonEvents(),requestAnimationFrame(()=>{this.elements.container.classList.add("comparison-visible")})}_generateComparisonRows(e,t){const n=[],s=' ',a=' ',r=this.isIndexedFormat?Object.keys(this.data.supported):Object.keys(this.data);for(const i of r){const r=this._getHostData(i),o="yes"===r[e],l="yes"===r[t];let c="neither-supported",d="Neither";o&&l?(c="both-supported",d="Both"):o?(c="service1-only",d=`${e} only`):l&&(c="service2-only",d=`${t} only`),n.push(`\n \n ${i} \n \n \n ${o?s:a}\n \n \n \n \n ${l?s:a}\n \n \n ${d} \n \n `)}return n.join("")}_calculateComparisonStats(e,t){let n=0,s=0,a=0;const r=this.isIndexedFormat?Object.keys(this.data.supported):Object.keys(this.data);for(const i of r){const r=this._getHostData(i),o="yes"===r[e],l="yes"===r[t];o&&l?n++:o?s++:l&&a++}return{shared:n,service1Only:s,service2Only:a}}_attachComparisonEvents(){const e=this.elements.container.querySelector("#close-compare");e&&e.addEventListener("click",()=>this._closeComparison()),this.elements.container.addEventListener("change",e=>{"comparison-filter"===e.target.name&&this._filterComparison(e.target.value)})}_filterComparison(e){const t=this.elements.container.querySelectorAll(".comparison-row");let n=0;t.forEach(t=>{const s=t.dataset.status,a="all"===e||"both"===e&&"both-supported"===s||"different"===e&&("service1-only"===s||"service2-only"===s);t.style.display=a?"":"none",a&&n++});let s=this.elements.container.querySelector(".filter-results");if(!s){s=document.createElement("div"),s.className="filter-results";const e=this.elements.container.querySelector(".table-wrapper");e.parentNode.insertBefore(s,e)}s.textContent=`Showing ${n} services`}_closeComparison(){this.elements.container.classList.add("comparison-hiding"),setTimeout(()=>{this.elements.container.style.display="none",this.elements.container.classList.remove("comparison-visible","comparison-hiding"),this.isComparing=!1,this._showEmptyState()},250)}_showEmptyState(){this.elements.container.innerHTML='\n \n
⚖️
\n
Ready to Compare \n
Select two services above to see a detailed comparison
\n
\n ',this.elements.container.style.display="block"}_showSameProviderWarning(){this.elements.container.innerHTML='\n \n
⚠️
\n
Same Service Selected \n
Please select two different services to compare
\n
\n ',this.elements.container.style.display="block"}}class PricingManager{constructor(e){this.container=document.getElementById(`${e}-table-container`),this.currentData=null,this._onMouseOver=this._onMouseOver.bind(this),this._onMouseOut=this._onMouseOut.bind(this),this._onMouseLeaveContainer=this._onMouseLeaveContainer.bind(this)}generatePricingTable(e={}){if(!this.container)return;if(!e.plans?.length)return void(this.container.innerHTML='Error: Invalid pricing data structure.
');this.currentData=e;const t=Object.keys(e.plans[0]).filter(e=>"name"!==e),n=document.createDocumentFragment(),s=document.createElement("div");s.className="table-wrapper";const a=document.createElement("table");a.id="pricing-table",a.className="pricing-table",a.setAttribute("aria-label","Service Pricing Table");const r=document.createElement("thead");r.innerHTML=`\n \n Plans \n ${t.map(e=>`\n \n \n \n `).join("")}\n \n `;const i=document.createElement("tbody");i.innerHTML=e.plans.map((e,n)=>`\n \n ${e.name} \n ${t.map(t=>{const n=e[t],s=!isNaN(parseFloat(n));return`\n \n \n ${n} \n ${s?'/month ':""}\n
\n \n `}).join("")}\n \n `).join(""),a.appendChild(r),a.appendChild(i),s.appendChild(a),n.appendChild(s),this.container.innerHTML="",this.container.appendChild(n),this._setupPricingFeatures()}_setupPricingFeatures(){this.container.addEventListener("mouseover",this._onMouseOver),this.container.addEventListener("mouseout",this._onMouseOut),this.container.addEventListener("mouseleave",this._onMouseLeaveContainer)}_onMouseOver(e){const t=e.target.closest(".price-cell");if(!t||!this.container.contains(t))return;const n=Array.from(t.parentNode.children).indexOf(t);n>=0&&this._highlightColumn(n+1)}_onMouseOut(e){const t=e.relatedTarget;t&&this.container.contains(t)||this._removeColumnHighlight()}_onMouseLeaveContainer(){this._removeColumnHighlight()}_highlightColumn(e){const t=this.container.querySelector("table");if(!t)return;const n=t.querySelectorAll(`tbody td:nth-child(${e+1}), thead th:nth-child(${e+1})`);this._removeColumnHighlight(),n.forEach(e=>e.classList.add("highlighted"))}_removeColumnHighlight(){this.container.querySelectorAll(".highlighted").forEach(e=>e.classList.remove("highlighted"))}}class PerformanceMonitor{constructor(){this.marks=new Map,this.mark("start")}mark(e){this.marks.set(e,performance.now())}measure(e,t="start"){const n=this.marks.get(t)||performance.now();return performance.now()-n}log(e,t="start"){console.log(`${e}: ${this.measure(e,t).toFixed(2)}ms`)}}function setupURLComparison(){const e=new URLSearchParams(window.location.search),t=e.get("compare"),n=e.get("with");t&&n&&requestAnimationFrame(()=>{const e=document.getElementById("provider1"),s=document.getElementById("provider2");e&&s&&(e.value=t,s.value=n,e.dispatchEvent(new Event("change")))})}function setupScrollAnimations(){const e=document.querySelectorAll(".section, .status-card, .referral-card");e.length&&Utils.animateOnScroll(e)}function setupOfflineDetection(){const e=()=>{console.log(navigator.onLine?"Connection restored":"You are offline. Some features may not work.")};window.addEventListener("online",e),window.addEventListener("offline",e)}function setupBackToTop(){const e=document.getElementById("backToTop");if(!e)return;const t=Utils.throttle(()=>{window.pageYOffset>300?e.classList.add("visible"):e.classList.remove("visible")},150);window.addEventListener("scroll",t,{passive:!0}),e.addEventListener("click",()=>window.scrollTo({top:0,behavior:"smooth"}))}function setupNavHighlight(){const e=Array.from(document.querySelectorAll('.header-nav .nav-link[href^="#"]'));if(!("IntersectionObserver"in window)||!e.length)return;const t=e.map(e=>document.querySelector(e.getAttribute("href"))).filter(Boolean),n=new Map;e.forEach(e=>n.set(e.getAttribute("href"),e));const s=new IntersectionObserver(t=>{t.forEach(t=>{const s=`#${t.target.id}`,a=n.get(s);a&&t.isIntersecting&&(e.forEach(e=>e.removeAttribute("aria-current")),a.setAttribute("aria-current","page"))})},{rootMargin:"-40% 0px -50% 0px",threshold:.1});t.forEach(e=>s.observe(e))}function setupAgeVerification(){const e=document.getElementById("age-verification-overlay"),t=document.getElementById("adult-content"),n=document.getElementById("confirm-age"),s=document.getElementById("deny-age"),a=document.querySelector(".page-wrapper"),r=document.getElementById("adult-hosts");if(!e||!r)return;const i=e.querySelector(".age-verification-modal"),o=e.querySelector(".age-verification-content h3");if(i&&(i.setAttribute("role","dialog"),i.setAttribute("aria-modal","true"),o)){const e="age-title";o.id=e,i.setAttribute("aria-labelledby",e)}const l=e=>{a&&("inert"in a?a.inert=e:a.setAttribute("aria-hidden",String(e)))};let c=null;const d=()=>{e.style.display="flex",e.setAttribute("aria-hidden","false"),l(!0),c=(e=>{const t=Array.from(e.querySelectorAll('a[href], area[href], button, input, select, textarea, details, summary, [tabindex]:not([tabindex="-1"])')).filter(e=>!e.hasAttribute("disabled")&&"true"!==e.getAttribute("aria-hidden")),n=t[0],a=t[t.length-1],r=e=>{if("Escape"===e.key)s?.click();else if("Tab"===e.key){if(0===t.length)return void e.preventDefault();e.shiftKey&&document.activeElement===n?(e.preventDefault(),a.focus()):e.shiftKey||document.activeElement!==a||(e.preventDefault(),n.focus())}};return e.addEventListener("keydown",r),n?.focus(),()=>e.removeEventListener("keydown",r)})(i||e)},h=()=>{e.style.display="none",e.setAttribute("aria-hidden","true"),l(!1),"function"==typeof c&&c()};if("true"===localStorage.getItem("age-confirmed")&&(e.style.display="none",t&&(t.style.display="block")),n?.addEventListener("click",()=>{localStorage.setItem("age-confirmed","true"),t&&(t.style.display="block"),h()}),s?.addEventListener("click",()=>{document.getElementById("what-are-debrid")?.scrollIntoView({behavior:"smooth"}),h()}),"IntersectionObserver"in window){new IntersectionObserver(e=>{e.forEach(e=>{e.isIntersecting&&"true"!==localStorage.getItem("age-confirmed")&&d()})},{threshold:.1}).observe(r)}}const loadingManager=new LoadingManager,performanceMonitor=new PerformanceMonitor;document.addEventListener("DOMContentLoaded",async()=>{performanceMonitor.mark("dom-ready"),performanceMonitor.log("DOM Ready");try{const e=new TableManager("file-hosts-table-container","host-search-input","clear-host-search"),t=new TableManager("adult-hosts-table-container","adult-host-search-input","clear-adult-host-search"),n=new PricingManager("pricing"),s=[loadingManager.show("#file-hosts-table-container","Loading file hosts..."),loadingManager.show("#adult-hosts-table-container","Loading adult hosts..."),loadingManager.show("#pricing-table-container","Loading pricing data...")],a=["./json/file-hosts-optimized.json","./json/adult-hosts-optimized.json","./json/pricing.json"],r=a.map(e=>fetch(e).then(t=>t.ok?t.json():Promise.reject(new Error(`HTTP ${t.status} for ${e}`))));(await Promise.allSettled(r)).forEach((r,i)=>{const o=["#file-hosts-table-container","#adult-hosts-table-container","#pricing-table-container"];if(loadingManager.hide(o[i],s[i]),"fulfilled"===r.status)switch(i){case 0:e.generateTable(r.value),new ComparisonManager("compare-table-container","provider1","provider2",r.value);break;case 1:t.generateTable(r.value);break;case 2:n.generatePricingTable(r.value)}else{console.error(`Error loading data (${a[i]}):`,r.reason);const e=document.querySelector(o[i]);e&&(e.innerHTML=`Failed to load data: ${a[i]}
`)}}),setupURLComparison(),setupScrollAnimations(),setupOfflineDetection(),setupBackToTop(),setupNavHighlight(),setupAgeVerification(),performanceMonitor.mark("fully-loaded"),performanceMonitor.log("Fully Loaded")}catch(e){console.error("Critical error:",e),loadingManager.hideAll()}}),window.addEventListener("error",e=>{console.error("Global error:",e.error||e.message||e)},{passive:!0}),window.addEventListener("unhandledrejection",e=>{console.error("Unhandled promise rejection:",e.reason)}),"serviceWorker"in navigator&&window.addEventListener("load",()=>{navigator.serviceWorker.register("/sw.js").catch(e=>{console.warn("Service worker registration failed:",e)})});
\ No newline at end of file
diff --git a/dist/js/app.js b/dist/js/app.js
index d5e5002..fa965e8 100644
--- a/dist/js/app.js
+++ b/dist/js/app.js
@@ -11,601 +11,734 @@
Utils
============================== */
const Utils = (() => {
- const debounceCache = new WeakMap();
-
- return {
- debounce(func, delay = 300, immediate = false) {
- if (debounceCache.has(func)) return debounceCache.get(func);
-
- let timeoutId;
- const debounced = function(...args) {
- const callNow = immediate && !timeoutId;
- clearTimeout(timeoutId);
-
- if (callNow) func.apply(this, args);
-
- timeoutId = setTimeout(() => {
- timeoutId = null;
- if (!immediate) func.apply(this, args);
- }, delay);
- };
-
- debounceCache.set(func, debounced);
- return debounced;
- },
-
- throttle(func, limit = 100) {
- let ticking = false;
- return function(...args) {
- if (!ticking) {
- func.apply(this, args);
- ticking = true;
- requestAnimationFrame(() => {
- setTimeout(() => { ticking = false; }, limit);
- });
- }
- };
- },
-
- animateOnScroll: (() => {
- let observer;
- return (elements, options = {}) => {
- if (!('IntersectionObserver' in window)) return;
- if (!observer) {
- observer = new IntersectionObserver((entries) => {
- entries.forEach(entry => {
- if (entry.isIntersecting) {
- entry.target.classList.add('animate-in');
- observer.unobserve(entry.target);
- }
- });
- }, {
- threshold: 0.1,
- rootMargin: '0px 0px -50px 0px',
- ...options
- });
- }
- elements.forEach(el => observer.observe(el));
- };
- })()
- };
+ const debounceMap = new WeakMap();
+ return {
+ debounce(func, wait = 300, immediate = false) {
+ if (debounceMap.has(func)) return debounceMap.get(func);
+ let timeout;
+ const debounced = function (...args) {
+ const callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ if (callNow) func.apply(this, args);
+ timeout = setTimeout(() => {
+ timeout = null;
+ if (!immediate) func.apply(this, args);
+ }, wait);
+ };
+ debounceMap.set(func, debounced);
+ return debounced;
+ },
+ throttle(func, limit = 100) {
+ let inThrottle = false;
+ return function (...args) {
+ if (!inThrottle) {
+ func.apply(this, args);
+ inThrottle = true;
+ requestAnimationFrame(() => {
+ setTimeout(() => inThrottle = false, limit);
+ });
+ }
+ };
+ },
+ animateOnScroll: (() => {
+ let observer;
+ return (elements, options = {}) => {
+ if (!('IntersectionObserver' in window)) return;
+ if (!observer) {
+ observer = new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ entry.target.classList.add("animate-in");
+ observer.unobserve(entry.target);
+ }
+ });
+ }, {
+ threshold: 0.1,
+ rootMargin: "0px 0px -50px 0px",
+ ...options
+ });
+ }
+ elements.forEach((element) => observer.observe(element));
+ };
+ })()
+ };
})();
-/* ==============================
- Loading Manager
- ============================== */
class LoadingManager {
- constructor() {
- this.activeLoaders = new Map();
- this.template = this.createTemplate();
- }
+ constructor() {
+ this.activeLoaders = new Map();
+ this.template = this.createTemplate();
+ }
- createTemplate() {
- const template = document.createElement('template');
- template.innerHTML = `
-
- `;
- return template;
- }
+ createTemplate() {
+ const template = document.createElement("template");
+ template.innerHTML = `
+
+ `;
+ return template;
+ }
- show(target, text = 'Loading...') {
- const targetEl = typeof target === 'string' ? document.querySelector(target) : target;
- if (!targetEl) return null;
+ show(target, text = "Loading...") {
+ const element = typeof target === "string" ? document.querySelector(target) : target;
+ if (!element) return null;
- const loaderId = `loader-${Date.now()}-${Math.random().toString(36).slice(2)}`;
- const loader = this.template.content.cloneNode(true).firstElementChild;
+ const loaderId = `loader-${Date.now()}-${Math.random().toString(36).slice(2)}`;
+ const loaderElement = this.template.content.cloneNode(true).firstElementChild;
+ loaderElement.dataset.loaderId = loaderId;
+ loaderElement.querySelector(".loading-text").textContent = text;
- loader.dataset.loaderId = loaderId;
- loader.querySelector('.loading-text').textContent = text;
+ const computedStyle = getComputedStyle(element);
+ if (computedStyle.position === "static") {
+ element.style.position = "relative";
+ }
- const prevPos = getComputedStyle(targetEl).position;
- if (prevPos === 'static' || !prevPos) targetEl.style.position = 'relative';
+ element.appendChild(loaderElement);
+ this.activeLoaders.set(loaderId, loaderElement);
- targetEl.appendChild(loader);
- this.activeLoaders.set(loaderId, loader);
+ requestAnimationFrame(() => loaderElement.classList.add("loading-overlay--visible"));
+ return loaderId;
+ }
- requestAnimationFrame(() => loader.classList.add('loading-overlay--visible'));
- return loaderId;
- }
+ hide(target, loaderId) {
+ const loaderElement = this.activeLoaders.get(loaderId);
+ if (loaderElement) {
+ loaderElement.classList.add("loading-overlay--hiding");
+ this.activeLoaders.delete(loaderId);
+ setTimeout(() => loaderElement.remove(), 250);
+ }
+ }
- hide(target, loaderId) {
- const loader = this.activeLoaders.get(loaderId);
- if (!loader) return;
- loader.classList.add('loading-overlay--hiding');
- this.activeLoaders.delete(loaderId);
- setTimeout(() => loader.remove(), 250);
- }
-
- hideAll() {
- this.activeLoaders.forEach((_, id) => this.hide(null, id));
- }
+ hideAll() {
+ this.activeLoaders.forEach((_, loaderId) => this.hide(null, loaderId));
+ }
}
-/* ==============================
- Table Manager (file/adult hosts)
- ============================== */
class TableManager {
- constructor(containerId, searchInputId, clearIconId, options = {}) {
- this.elements = {
- container: document.getElementById(containerId),
- searchInput: document.getElementById(searchInputId),
- clearIcon: document.getElementById(clearIconId)
- };
+ constructor(containerId, searchInputId, clearIconId, options = {}) {
+ this.elements = {
+ container: document.getElementById(containerId),
+ searchInput: document.getElementById(searchInputId),
+ clearIcon: document.getElementById(clearIconId)
+ };
- this.options = { sortable: true, filterable: true, pagination: false, itemsPerPage: 50, ...options };
- this.state = {
- currentData: {},
- filteredData: {},
- currentSort: { column: null, direction: 'asc' },
- currentPage: 1
- };
+ this.options = {
+ sortable: true,
+ filterable: true,
+ pagination: false,
+ itemsPerPage: 50,
+ ...options
+ };
- this.handleSearch = Utils.debounce(this._performSearch.bind(this), 250);
- this.handleSort = this._performSort.bind(this);
+ this.state = {
+ currentData: {},
+ filteredData: {},
+ currentSort: { column: null, direction: "asc" },
+ currentPage: 1,
+ services: [], // Store services array for indexed format
+ isIndexedFormat: false // Flag to track data format
+ };
- this._init();
- }
-
- _init() {
- if (this.elements.searchInput) {
- this.elements.searchInput.addEventListener('input', this.handleSearch);
- this.elements.searchInput.addEventListener('keydown', e => {
- if (e.key === 'Escape') this._clearSearch();
- });
+ this.handleSearch = Utils.debounce(this._performSearch.bind(this), 250);
+ this.handleSort = this._performSort.bind(this);
+ this._init();
}
- if (this.elements.clearIcon) {
- this.elements.clearIcon.addEventListener('click', () => this._clearSearch());
- }
- }
-
- generateTable(data = {}) {
- if (!this.elements.container) return;
- if (!Object.keys(data).length) {
- this.elements.container.innerHTML = '';
- return;
- }
-
- this.state.currentData = data;
- this.state.filteredData = { ...data };
-
- const firstKey = Object.keys(data)[0];
- const providers = firstKey ? Object.keys(data[firstKey]) : [];
- const tableId = this.elements.container.id.replace('-container', '');
-
- const fragment = document.createDocumentFragment();
- const wrapper = document.createElement('div');
- wrapper.className = 'table-wrapper';
-
- const table = document.createElement('table');
- table.id = tableId;
- table.className = 'enhanced-table';
- table.setAttribute('aria-label', 'Service Comparison Table');
-
- const thead = document.createElement('thead');
- thead.innerHTML = this._generateTableHeader(providers);
- table.appendChild(thead);
-
- const tbody = document.createElement('tbody');
- tbody.innerHTML = this._generateTableRows(this.state.filteredData, providers);
- table.appendChild(tbody);
-
- wrapper.appendChild(table);
- fragment.appendChild(wrapper);
-
- this.elements.container.innerHTML = '';
- this.elements.container.appendChild(fragment);
-
- this._attachTableEvents();
- }
-
- _generateTableHeader(providers) {
- return `
-
-
- Service Name
-
-
-
-
- ${providers.map(provider => `
-
- ${provider}
-
-
-
-
- `).join('')}
-
- `;
- }
-
- _generateTableRows(data, providers) {
- const rows = [];
- for (const [host, providerData] of Object.entries(data)) {
- rows.push(`
-
-
-
- ${host}
-
-
- ${providers.map(provider => {
- const isSupported = providerData[provider] === 'yes';
- return `
-
-
- ${isSupported
- ? ' '
- : ' '
- }
-
-
- `;
- }).join('')}
-
- `);
- }
- return rows.join('');
- }
-
- _attachTableEvents() {
- if (!this.options.sortable) return;
- const thead = this.elements.container.querySelector('thead');
- if (thead) {
- thead.addEventListener('click', this.handleSort);
- thead.addEventListener('keydown', e => {
- const target = e.target.closest('.sortable');
- if (!target) return;
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- this.handleSort({ target });
+ _init() {
+ if (this.elements.searchInput) {
+ this.elements.searchInput.addEventListener("input", this.handleSearch);
+ this.elements.searchInput.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") this._clearSearch();
+ });
}
- });
- }
- }
- _performSearch() {
- const term = (this.elements.searchInput?.value || '').toLowerCase().trim();
-
- if (!term) {
- this.state.filteredData = { ...this.state.currentData };
- if (this.elements.clearIcon) this.elements.clearIcon.style.display = 'none';
- } else {
- this.state.filteredData = Object.fromEntries(
- Object.entries(this.state.currentData).filter(([host]) => host.toLowerCase().includes(term))
- );
- if (this.elements.clearIcon) this.elements.clearIcon.style.display = 'block';
+ if (this.elements.clearIcon) {
+ this.elements.clearIcon.addEventListener("click", () => this._clearSearch());
+ }
}
- this._updateTableContent();
- this._updateSearchResults(term);
- }
-
- _clearSearch() {
- if (this.elements.searchInput) this.elements.searchInput.value = '';
- this.state.filteredData = { ...this.state.currentData };
- if (this.elements.clearIcon) this.elements.clearIcon.style.display = 'none';
- this._updateTableContent();
- this._updateSearchResults('');
- }
-
- _updateSearchResults(searchTerm) {
- if (!this.elements.container) return;
- let indicator = this.elements.container.querySelector('.search-results');
-
- if (searchTerm) {
- const resultCount = Object.keys(this.state.filteredData).length;
- const totalCount = Object.keys(this.state.currentData).length;
-
- if (!indicator) {
- indicator = document.createElement('div');
- indicator.className = 'search-results';
- this.elements.container.insertBefore(indicator, this.elements.container.firstChild);
- }
- indicator.textContent = `Showing ${resultCount} of ${totalCount} services`;
- indicator.style.display = 'block';
- indicator.style.textAlign = 'center';
- indicator.style.padding = '0.5em 0';
- } else if (indicator) {
- indicator.style.display = 'none';
+ // 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;
}
- }
- _performSort(event) {
- const header = event.target.closest('.sortable');
- if (!header) return;
+ // Modified method to handle both formats
+ generateTable(data = {}) {
+ if (!this.elements.container) return;
- const column = header.dataset.column;
- const { currentSort } = this.state;
- const newDirection = currentSort.column === column && currentSort.direction === 'asc' ? 'desc' : 'asc';
+ // 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) : [];
+ }
- this.state.currentSort = { column, direction: newDirection };
+ if (!Object.keys(processedData).length) {
+ this.elements.container.innerHTML = '';
+ return;
+ }
- this.elements.container.querySelectorAll('th.sortable').forEach(th => {
- th.setAttribute('aria-sort', 'none');
- th.classList.remove('sorted-asc', 'sorted-desc');
- });
+ this.state.currentData = processedData;
+ this.state.filteredData = { ...processedData };
- header.setAttribute('aria-sort', newDirection === 'asc' ? 'ascending' : 'descending');
- header.classList.add(`sorted-${newDirection}`);
+ const firstHostKey = Object.keys(processedData)[0];
+ const columns = firstHostKey ? Object.keys(processedData[firstHostKey]) : [];
+ const tableId = this.elements.container.id.replace("-container", "");
- this._sortData();
- this._updateTableContent();
- }
+ const fragment = document.createDocumentFragment();
+ const wrapper = document.createElement("div");
+ wrapper.className = "table-wrapper";
- _sortData() {
- const { column, direction } = this.state.currentSort;
- const entries = Object.entries(this.state.filteredData);
+ const table = document.createElement("table");
+ table.id = tableId;
+ table.className = "enhanced-table";
+ table.setAttribute("aria-label", "Service Comparison Table");
- entries.sort(([hostA, dataA], [hostB, dataB]) => {
- const valueA = column === 'service' ? hostA.toLowerCase() : (dataA[column] === 'yes' ? 1 : 0);
- const valueB = column === 'service' ? hostB.toLowerCase() : (dataB[column] === 'yes' ? 1 : 0);
+ const thead = document.createElement("thead");
+ thead.innerHTML = this._generateTableHeader(columns);
+ table.appendChild(thead);
- const comparison = valueA > valueB ? 1 : valueA < valueB ? -1 : 0;
- return direction === 'asc' ? comparison : -comparison;
- });
+ const tbody = document.createElement("tbody");
+ tbody.innerHTML = this._generateTableRows(this.state.filteredData, columns);
+ table.appendChild(tbody);
- this.state.filteredData = Object.fromEntries(entries);
- }
+ wrapper.appendChild(table);
+ fragment.appendChild(wrapper);
- _updateTableContent() {
- const tbody = this.elements.container.querySelector('tbody');
- if (!tbody) return;
- const firstKey = Object.keys(this.state.currentData)[0];
- const providers = firstKey ? Object.keys(this.state.currentData[firstKey]) : [];
- requestAnimationFrame(() => {
- tbody.innerHTML = this._generateTableRows(this.state.filteredData, providers);
- });
- }
+ this.elements.container.innerHTML = "";
+ this.elements.container.appendChild(fragment);
+
+ this._attachTableEvents();
+ }
+
+ _generateTableHeader(columns) {
+ return `
+
+
+ Service Name
+
+
+
+
+ ${columns.map(column => `
+
+ ${column}
+
+
+
+
+ `).join("")}
+
+ `;
+ }
+
+ _generateTableRows(data, columns) {
+ const rows = [];
+ for (const [host, hostData] of Object.entries(data)) {
+ rows.push(`
+
+
+
+ ${host}
+
+
+ ${columns.map(column => {
+ const isSupported = hostData[column] === "yes";
+ return `
+
+
+ ${isSupported ?
+ ' ' :
+ ' '
+ }
+
+
+ `;
+ }).join("")}
+
+ `);
+ }
+ return rows.join("");
+ }
+
+ _attachTableEvents() {
+ if (!this.options.sortable) return;
+
+ const thead = this.elements.container.querySelector("thead");
+ if (thead) {
+ thead.addEventListener("click", this.handleSort);
+ thead.addEventListener("keydown", (e) => {
+ const sortableHeader = e.target.closest(".sortable");
+ if (sortableHeader && (e.key === "Enter" || e.key === " ")) {
+ e.preventDefault();
+ this.handleSort({ target: sortableHeader });
+ }
+ });
+ }
+ }
+
+ _performSearch() {
+ const searchTerm = (this.elements.searchInput?.value || "").toLowerCase().trim();
+
+ if (searchTerm) {
+ this.state.filteredData = Object.fromEntries(
+ Object.entries(this.state.currentData).filter(([host]) =>
+ host.toLowerCase().includes(searchTerm)
+ )
+ );
+ 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._updateSearchResults(searchTerm);
+ }
+
+ _clearSearch() {
+ if (this.elements.searchInput) {
+ this.elements.searchInput.value = "";
+ }
+ this.state.filteredData = { ...this.state.currentData };
+ if (this.elements.clearIcon) {
+ this.elements.clearIcon.style.display = "none";
+ }
+ this._updateTableContent();
+ this._updateSearchResults("");
+ }
+
+ _updateSearchResults(searchTerm) {
+ if (!this.elements.container) return;
+
+ let resultsElement = this.elements.container.querySelector(".search-results");
+
+ if (searchTerm) {
+ const filteredCount = Object.keys(this.state.filteredData).length;
+ const totalCount = Object.keys(this.state.currentData).length;
+
+ if (!resultsElement) {
+ resultsElement = document.createElement("div");
+ resultsElement.className = "search-results";
+ this.elements.container.insertBefore(resultsElement, this.elements.container.firstChild);
+ }
+
+ resultsElement.textContent = `Showing ${filteredCount} of ${totalCount} services`;
+ resultsElement.style.display = "block";
+ resultsElement.style.textAlign = "center";
+ resultsElement.style.padding = "0.5em 0";
+ } else if (resultsElement) {
+ resultsElement.style.display = "none";
+ }
+ }
+
+ _performSort(event) {
+ const sortableHeader = event.target.closest(".sortable");
+ if (!sortableHeader) return;
+
+ const column = sortableHeader.dataset.column;
+ const { currentSort } = this.state;
+ const direction = currentSort.column === column && currentSort.direction === "asc" ? "desc" : "asc";
+
+ this.state.currentSort = { column, direction };
+
+ // Update UI
+ this.elements.container.querySelectorAll("th.sortable").forEach(header => {
+ header.setAttribute("aria-sort", "none");
+ header.classList.remove("sorted-asc", "sorted-desc");
+ });
+
+ sortableHeader.setAttribute("aria-sort", direction === "asc" ? "ascending" : "descending");
+ sortableHeader.classList.add(`sorted-${direction}`);
+
+ this._sortData();
+ this._updateTableContent();
+ }
+
+ _sortData() {
+ const { column, direction } = this.state.currentSort;
+ const entries = Object.entries(this.state.filteredData);
+
+ entries.sort(([hostA, dataA], [hostB, dataB]) => {
+ let valueA, valueB;
+
+ if (column === "service") {
+ 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);
+ }
+
+ _updateTableContent() {
+ const tbody = this.elements.container.querySelector("tbody");
+ if (!tbody) return;
+
+ const firstHostKey = Object.keys(this.state.currentData)[0];
+ const columns = firstHostKey ? Object.keys(this.state.currentData[firstHostKey]) : [];
+
+ requestAnimationFrame(() => {
+ tbody.innerHTML = this._generateTableRows(this.state.filteredData, columns);
+ });
+ }
}
/* ==============================
Comparison Manager
============================== */
class ComparisonManager {
- constructor(containerId, select1Id, select2Id, data) {
- this.elements = {
- container: document.getElementById(containerId),
- select1: document.getElementById(select1Id),
- select2: document.getElementById(select2Id)
- };
- this.data = data;
- this.isComparing = false;
-
- this.handleCompare = this._generateCompareTable.bind(this);
- this._init();
- }
-
- _init() {
- this._populateDropdowns();
-
- this.elements.select1?.addEventListener('change', this.handleCompare);
- this.elements.select2?.addEventListener('change', this.handleCompare);
-
- document.addEventListener('keydown', e => {
- if (this.isComparing && e.key === 'Escape') this._closeComparison();
- });
- }
-
- _populateDropdowns() {
- const firstKey = Object.keys(this.data)[0];
- const providers = firstKey ? Object.keys(this.data[firstKey]) : [];
- const optionsHTML = 'Choose a service... ' +
- providers.map(p => `${p} `).join('');
-
- [this.elements.select1, this.elements.select2].forEach(select => {
- if (select) select.innerHTML = optionsHTML;
- });
- }
-
- _generateCompareTable() {
- const provider1 = this.elements.select1?.value;
- const provider2 = this.elements.select2?.value;
-
- if (!provider1 || !provider2) {
- this._showEmptyState();
- return;
- }
- if (provider1 === provider2) {
- this._showSameProviderWarning();
- return;
+ constructor(containerId, select1Id, select2Id, data) {
+ this.elements = {
+ container: document.getElementById(containerId),
+ select1: document.getElementById(select1Id),
+ select2: document.getElementById(select2Id)
+ };
+
+ this.data = data;
+ this.isComparing = false;
+ 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.isComparing = true;
- const loaderId = loadingManager.show(this.elements.container, 'Generating comparison...');
-
- requestAnimationFrame(() => {
- this._renderComparisonTable(provider1, provider2);
- loadingManager.hide(this.elements.container, loaderId);
- });
- }
-
- _renderComparisonTable(provider1, provider2) {
- const stats = this._calculateComparisonStats(provider1, provider2);
- const fragment = document.createDocumentFragment();
- const wrapper = document.createElement('div');
-
- wrapper.innerHTML = `
-
-
-
-
-
- All Services
-
-
-
- Supported by Both
-
-
-
- Different Support
-
-
-
-
-
-
-
- Service Name
-
-
- Status
-
-
-
- ${this._generateComparisonRows(provider1, provider2)}
-
-
-
- `;
-
- fragment.appendChild(wrapper);
- this.elements.container.innerHTML = '';
- this.elements.container.appendChild(fragment);
- this.elements.container.style.display = 'block';
- this._attachComparisonEvents();
-
- requestAnimationFrame(() => {
- this.elements.container.classList.add('comparison-visible');
- });
- }
-
- _generateComparisonRows(provider1, provider2) {
- const rows = [];
- const checkSvg = ' ';
- const crossSvg = ' ';
-
- for (const [host, providerData] of Object.entries(this.data)) {
- const support1 = providerData[provider1] === 'yes';
- const support2 = providerData[provider2] === 'yes';
-
- let statusClass = 'neither-supported';
- let statusText = 'Neither';
- if (support1 && support2) { statusClass = 'both-supported'; statusText = 'Both'; }
- else if (support1) { statusClass = 'provider1-only'; statusText = `${provider1} only`; }
- else if (support2) { statusClass = 'provider2-only'; statusText = `${provider2} only`; }
-
- rows.push(`
-
- ${host}
-
- ${support1 ? checkSvg : crossSvg}
-
-
- ${support2 ? checkSvg : crossSvg}
-
- ${statusText}
-
- `);
+ _init() {
+ this._populateDropdowns();
+
+ if (this.elements.select1) {
+ this.elements.select1.addEventListener("change", this.handleCompare);
+ }
+
+ if (this.elements.select2) {
+ this.elements.select2.addEventListener("change", this.handleCompare);
+ }
+
+ document.addEventListener("keydown", (e) => {
+ if (this.isComparing && e.key === "Escape") {
+ this._closeComparison();
+ }
+ });
}
- return rows.join('');
- }
- _calculateComparisonStats(provider1, provider2) {
- let shared = 0, provider1Only = 0, provider2Only = 0;
- for (const providerData of Object.values(this.data)) {
- const s1 = providerData[provider1] === 'yes';
- const s2 = providerData[provider2] === 'yes';
- if (s1 && s2) shared++;
- else if (s1) provider1Only++;
- else if (s2) provider2Only++;
+ _populateDropdowns() {
+ // Use the stored services array
+ const optionsHtml = 'Choose a service... ' +
+ this.services.map(service =>
+ `${service} `
+ ).join("");
+
+ [this.elements.select1, this.elements.select2].forEach(select => {
+ if (select) {
+ select.innerHTML = optionsHtml;
+ }
+ });
}
- return { shared, provider1Only, provider2Only };
- }
- _attachComparisonEvents() {
- const closeBtn = this.elements.container.querySelector('#close-compare');
- closeBtn?.addEventListener('click', () => this._closeComparison());
-
- this.elements.container.addEventListener('change', e => {
- if (e.target.name === 'comparison-filter') {
- this._filterComparison(e.target.value);
- }
- });
- }
-
- _filterComparison(filter) {
- const rows = this.elements.container.querySelectorAll('.comparison-row');
- let visibleCount = 0;
-
- rows.forEach(row => {
- const status = row.dataset.status;
- const show = filter === 'all' ||
- (filter === 'both' && status === 'both-supported') ||
- (filter === 'different' && (status === 'provider1-only' || status === 'provider2-only'));
- row.style.display = show ? '' : 'none';
- if (show) visibleCount++;
- });
-
- let indicator = this.elements.container.querySelector('.filter-results');
- if (!indicator) {
- indicator = document.createElement('div');
- indicator.className = 'filter-results';
- const tableWrapper = this.elements.container.querySelector('.table-wrapper');
- tableWrapper.parentNode.insertBefore(indicator, tableWrapper);
+ // Transform indexed data to regular format for comparison
+ _getHostData(hostName) {
+ 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] || {};
+ }
}
- indicator.textContent = `Showing ${visibleCount} services`;
- }
- _closeComparison() {
- this.elements.container.classList.add('comparison-hiding');
- setTimeout(() => {
- this.elements.container.style.display = 'none';
- this.elements.container.classList.remove('comparison-visible', 'comparison-hiding');
- this.isComparing = false;
- this._showEmptyState();
- }, 250);
- }
+ _generateCompareTable() {
+ const service1 = this.elements.select1?.value;
+ const service2 = this.elements.select2?.value;
+
+ if (!service1 || !service2) {
+ this._showEmptyState();
+ return;
+ }
+
+ if (service1 === service2) {
+ this._showSameProviderWarning();
+ return;
+ }
+
+ this.isComparing = true;
+ const loaderId = loadingManager.show(this.elements.container, "Generating comparison...");
+
+ requestAnimationFrame(() => {
+ this._renderComparisonTable(service1, service2);
+ loadingManager.hide(this.elements.container, loaderId);
+ });
+ }
- _showEmptyState() {
- this.elements.container.innerHTML = `
-
-
⚖️
-
Ready to Compare
-
Select two services above to see a detailed comparison
-
- `;
- this.elements.container.style.display = 'block';
- }
+ _renderComparisonTable(service1, service2) {
+ const stats = this._calculateComparisonStats(service1, service2);
+
+ const fragment = document.createDocumentFragment();
+ const wrapper = document.createElement("div");
+ wrapper.innerHTML = `
+
- _showSameProviderWarning() {
- this.elements.container.innerHTML = `
-
-
⚠️
-
Same Service Selected
-
Please select two different services to compare
-
- `;
- this.elements.container.style.display = 'block';
- }
+
+
+
+ All Services
+
+
+
+ Supported by Both
+
+
+
+ Different Support
+
+
+
+
+
+
+
+ Service Name
+
+
+ Status
+
+
+
+ ${this._generateComparisonRows(service1, service2)}
+
+
+
+ `;
+
+ fragment.appendChild(wrapper);
+ this.elements.container.innerHTML = "";
+ this.elements.container.appendChild(fragment);
+ this.elements.container.style.display = "block";
+
+ this._attachComparisonEvents();
+
+ requestAnimationFrame(() => {
+ this.elements.container.classList.add("comparison-visible");
+ });
+ }
+
+ _generateComparisonRows(service1, service2) {
+ const rows = [];
+ const checkIcon = ' ';
+ const crossIcon = ' ';
+
+ // Get all hosts (keys from the original data structure)
+ const hosts = this.isIndexedFormat ? Object.keys(this.data.supported) : Object.keys(this.data);
+
+ for (const host of hosts) {
+ const hostData = this._getHostData(host);
+ const supported1 = hostData[service1] === "yes";
+ const supported2 = hostData[service2] === "yes";
+
+ 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(`
+
+ ${host}
+
+
+ ${supported1 ? checkIcon : crossIcon}
+
+
+
+
+ ${supported2 ? checkIcon : crossIcon}
+
+
+ ${statusText}
+
+ `);
+ }
+
+ return rows.join("");
+ }
+
+ _calculateComparisonStats(service1, service2) {
+ let shared = 0;
+ let service1Only = 0;
+ let service2Only = 0;
+
+ // Get all hosts
+ const hosts = this.isIndexedFormat ? Object.keys(this.data.supported) : Object.keys(this.data);
+
+ 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, service1Only, service2Only };
+ }
+
+ _attachComparisonEvents() {
+ const closeBtn = this.elements.container.querySelector("#close-compare");
+ if (closeBtn) {
+ closeBtn.addEventListener("click", () => this._closeComparison());
+ }
+
+ this.elements.container.addEventListener("change", (e) => {
+ if (e.target.name === "comparison-filter") {
+ this._filterComparison(e.target.value);
+ }
+ });
+ }
+
+ _filterComparison(filterValue) {
+ const rows = this.elements.container.querySelectorAll(".comparison-row");
+ let visibleCount = 0;
+
+ rows.forEach(row => {
+ const status = row.dataset.status;
+ const shouldShow =
+ filterValue === "all" ||
+ (filterValue === "both" && status === "both-supported") ||
+ (filterValue === "different" && (status === "service1-only" || status === "service2-only"));
+
+ row.style.display = shouldShow ? "" : "none";
+ if (shouldShow) visibleCount++;
+ });
+
+ let resultsElement = this.elements.container.querySelector(".filter-results");
+ if (!resultsElement) {
+ resultsElement = document.createElement("div");
+ resultsElement.className = "filter-results";
+ const tableWrapper = this.elements.container.querySelector(".table-wrapper");
+ tableWrapper.parentNode.insertBefore(resultsElement, tableWrapper);
+ }
+
+ resultsElement.textContent = `Showing ${visibleCount} services`;
+ }
+
+ _closeComparison() {
+ this.elements.container.classList.add("comparison-hiding");
+ setTimeout(() => {
+ this.elements.container.style.display = "none";
+ this.elements.container.classList.remove("comparison-visible", "comparison-hiding");
+ this.isComparing = false;
+ this._showEmptyState();
+ }, 250);
+ }
+
+ _showEmptyState() {
+ this.elements.container.innerHTML = `
+
+
⚖️
+
Ready to Compare
+
Select two services above to see a detailed comparison
+
+ `;
+ this.elements.container.style.display = "block";
+ }
+
+ _showSameProviderWarning() {
+ this.elements.container.innerHTML = `
+
+
⚠️
+
Same Service Selected
+
Please select two different services to compare
+
+ `;
+ this.elements.container.style.display = "block";
+ }
}
-
/* ==============================
Pricing Manager
============================== */
@@ -936,7 +1069,7 @@ document.addEventListener('DOMContentLoaded', async () => {
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 =>
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 {
console.error(`Error loading data (${dataUrls[index]}):`, result.reason);
const el = document.querySelector(containers[index]);
- if (el) el.innerHTML = `Failed to load data: ${dataUrls[index]}
`;
+ if (el) el.innerHTML = `Failed to load data: ${dataUrls[index]}
`;
}
});
diff --git a/dist/scripts/replace-assets.js b/dist/scripts/replace-assets.js
deleted file mode 100644
index 2d17b35..0000000
--- a/dist/scripts/replace-assets.js
+++ /dev/null
@@ -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');
\ No newline at end of file
diff --git a/package.json b/package.json
index 455fa7b..611bfe9 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,8 @@
"scripts": {
"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",
- "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"
},
"author": "fynks",
diff --git a/scripts/json-optimizer.sh b/scripts/json-optimizer.sh
new file mode 100755
index 0000000..ab25304
--- /dev/null
+++ b/scripts/json-optimizer.sh
@@ -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 [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
\ No newline at end of file