debrid-services-comparison/dist/js/app-min.js
copilot-swe-agent[bot] b61212892f Initial analysis and planning for modern mobile-first redesign
Co-authored-by: fynks <75840152+fynks@users.noreply.github.com>
2025-08-19 03:57:25 +00:00

1 line
No EOL
20 KiB
JavaScript

const Utils={debounce:(e,t=300,n=!1)=>{let a;return function(...s){const r=n&&!a;clearTimeout(a),a=setTimeout(()=>{a=null,n||e.apply(this,s)},t),r&&e.apply(this,s)}},throttle:(e,t)=>{let n;return function(){const a=arguments,s=this;n||(e.apply(s,a),n=!0,setTimeout(()=>n=!1,t))}},animateOnScroll:(e,t={})=>{const n=new IntersectionObserver(e=>{e.forEach(e=>{e.isIntersecting&&(e.target.classList.add("animate-in"),n.unobserve(e.target))})},{threshold:.1,rootMargin:"0px 0px -50px 0px",...t});e.forEach(e=>n.observe(e))}};class LoadingManager{constructor(){this.activeLoaders=new Set}show(e,t="Loading..."){const n=Date.now().toString(),a=document.createElement("div");return a.className="loading-overlay",a.setAttribute("data-loader-id",n),a.innerHTML=`\n <div class="loading-content">\n <div class="loading-spinner">\n <div class="spinner-ring"></div>\n <div class="spinner-ring"></div>\n <div class="spinner-ring"></div>\n </div>\n <p class="loading-text">${t}</p>\n </div>\n `,"string"==typeof e&&(e=document.querySelector(e)),e.style.position="relative",e.appendChild(a),this.activeLoaders.add(n),requestAnimationFrame(()=>{a.classList.add("loading-overlay--visible")}),n}hide(e,t){"string"==typeof e&&(e=document.querySelector(e));const n=e.querySelector(`[data-loader-id="${t}"]`);n&&(n.classList.add("loading-overlay--hiding"),this.activeLoaders.delete(t),setTimeout(()=>{n.parentNode&&e.removeChild(n)},300))}hideAll(){this.activeLoaders.forEach(e=>{const t=document.querySelector(`[data-loader-id="${e}"]`);t&&this.hide(t.parentNode,e)})}}class TableManager{constructor(e,t,n,a={}){this.container=document.querySelector(`#${e}`),this.searchInput=document.querySelector(`#${t}`),this.clearIcon=document.querySelector(`#${n}`),this.options={sortable:!0,filterable:!0,pagination:!1,itemsPerPage:50,...a},this.currentData={},this.filteredData={},this.currentSort={column:null,direction:"asc"},this.currentPage=1,this._setupSearchFunctionality(),this._setupKeyboardNavigation()}generateTable(e={}){if(!Object.keys(e).length)return void(this.container.innerHTML='<div class="empty-state"><p>No data available.</p></div>');this.currentData=e,this.filteredData={...e};const t=Object.keys(e[Object.keys(e)[0]]),n=this.container.id.replace("-container","");this.container.innerHTML=`\n <div class="table-wrapper">\n <table id="${n}" class="enhanced-table" aria-label="Service Comparison Table">\n <thead>\n <tr>\n <th class="sortable" data-column="service" tabindex="0" role="columnheader" aria-sort="none">\n <span>Service Name</span>\n <svg class="sort-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\n <path d="M12 5v14M5 12l7-7 7 7"/>\n </svg>\n </th>\n ${t.map(e=>`\n <th class="sortable" data-column="${e}" tabindex="0" role="columnheader" aria-sort="none">\n <span>${e}</span>\n <svg class="sort-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\n <path d="M12 5v14M5 12l7-7 7 7"/>\n </svg>\n </th>\n `).join("")}\n </tr>\n </thead>\n <tbody>\n ${this._generateTableRows(this.filteredData,t)}\n </tbody>\n </table>\n </div>\n ${this.options.pagination?this._createPagination():""}\n `,this._setupTableFeatures(),this._setupAccessibility()}_generateTableRows(e,t){return Object.entries(e).map(([e,n])=>`\n <tr data-host="${e.toLowerCase()}" role="row">\n <td class="service-cell" role="gridcell">\n <div class="service-info">\n <span class="service-name">${e}</span>\n </div>\n </td>\n ${t.map(e=>`\n <td class="status-cell" role="gridcell" data-status="${n[e]}">\n <span class="status-indicator ${"yes"===n[e]?"supported":"not-supported"}" \n aria-label="${"yes"===n[e]?"Supported":"Not supported"}">\n ${"yes"===n[e]?'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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 width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>'}\n </span>\n </td>\n `).join("")}\n </tr>\n `).join("")}_setupSearchFunctionality(){const e=Utils.debounce(()=>{const e=this.searchInput.value.toLowerCase().trim();e?(this.filteredData={},Object.entries(this.currentData).forEach(([t,n])=>{t.toLowerCase().includes(e)&&(this.filteredData[t]=n)}),this.clearIcon.style.display="block"):(this.filteredData={...this.currentData},this.clearIcon.style.display="none"),this._updateTableContent(),this._updateSearchResults(e)},300);this.searchInput?.addEventListener("input",e),this.searchInput?.addEventListener("keydown",e=>{"Escape"===e.key&&this._clearSearch()}),this.clearIcon?.addEventListener("click",()=>{this._clearSearch()})}_clearSearch(){this.searchInput.value="",this.filteredData={...this.currentData},this.clearIcon.style.display="none",this._updateTableContent(),this._updateSearchResults("")}_updateSearchResults(e){const t=Object.keys(this.filteredData).length,n=Object.keys(this.currentData).length;let a=this.container.querySelector(".search-results");a||(a=document.createElement("div"),a.className="search-results",this.container.insertBefore(a,this.container.querySelector(".table-wrapper"))),e?(a.textContent=`Showing ${t} of ${n} services`,a.style.display="block"):a.style.display="none"}_setupTableFeatures(){this.options.sortable&&this.container.querySelectorAll("th.sortable").forEach(e=>{e.addEventListener("click",()=>this._handleSort(e)),e.addEventListener("keydown",t=>{"Enter"!==t.key&&" "!==t.key||(t.preventDefault(),this._handleSort(e))})})}_handleSort(e){const t=e.getAttribute("data-column"),n="asc"===(this.currentSort.column===t?this.currentSort.direction:"asc")?"desc":"asc";this.currentSort={column:t,direction:n},this.container.querySelectorAll("th.sortable").forEach(e=>{e.setAttribute("aria-sort","none"),e.classList.remove("sorted-asc","sorted-desc")}),e.setAttribute("aria-sort","asc"===n?"ascending":"descending"),e.classList.add(`sorted-${n}`),this._sortData(),this._updateTableContent()}_sortData(){const{column:e,direction:t}=this.currentSort,n=Object.entries(this.filteredData);n.sort(([n,a],[s,r])=>{let i,o;return"service"===e?(i=n.toLowerCase(),o=s.toLowerCase()):(i="yes"===a[e]?1:0,o="yes"===r[e]?1:0),"asc"===t?i>o?1:i<o?-1:0:i<o?1:i>o?-1:0}),this.filteredData=Object.fromEntries(n)}_updateTableContent(){const e=this.container.querySelector("tbody"),t=Object.keys(this.currentData[Object.keys(this.currentData)[0]]);e&&(e.innerHTML=this._generateTableRows(this.filteredData,t),this._setupTableFeatures())}_setupKeyboardNavigation(){this.container.addEventListener("keydown",e=>{!e.target.matches("th.sortable")||"Enter"!==e.key&&" "!==e.key||(e.preventDefault(),this._handleSort(e.target))})}_setupAccessibility(){const e=this.container.querySelector("table");e&&(e.setAttribute("role","grid"),e.setAttribute("aria-label","Debrid services comparison table"))}_createPagination(){return`\n <div class="pagination">\n <button class="pagination-btn" data-action="prev" aria-label="Previous page">Previous</button>\n <span class="pagination-info">Page ${this.currentPage}</span>\n <button class="pagination-btn" data-action="next" aria-label="Next page">Next</button>\n </div>\n `}}class ComparisonManager{constructor(e,t,n,a){this.container=document.querySelector(`#${e}`),this.select1=document.querySelector(`#${t}`),this.select2=document.querySelector(`#${n}`),this.data=a,this.isComparing=!1,this._populateDropdowns(),this._setupEventListeners(),this._setupKeyboardShortcuts()}_populateDropdowns(){const e=Object.keys(this.data[Object.keys(this.data)[0]]);[this.select1,this.select2].forEach((t,n)=>{t.innerHTML='<option value="">Loading services...</option>',setTimeout(()=>{t.innerHTML='<option value="">Choose a service...</option>',e.forEach(e=>{const n=document.createElement("option");n.value=e,n.textContent=e,t.appendChild(n)})},100*(n+1))})}_generateCompareTable(){const e=this.select1.value,t=this.select2.value;if(!e||!t)return void this._showEmptyState();if(e===t)return void this._showSameProviderWarning();this.isComparing=!0;const n=loadingManager.show(this.container,"Generating comparison...");setTimeout(()=>{this._renderComparisonTable(e,t),loadingManager.hide(this.container,n),this._trackComparison(e,t)},500)}_renderComparisonTable(e,t){const n=this._calculateComparisonStats(e,t);this.container.innerHTML=`\n <div class="comparison-header">\n <h3>Comparing ${e} vs ${t}</h3>\n <div class="comparison-stats">\n <div class="stat">\n <span class="stat-label">Shared Support</span>\n <span class="stat-value">${n.shared}</span>\n </div>\n <div class="stat">\n <span class="stat-label">${e} Only</span>\n <span class="stat-value">${n.provider1Only}</span>\n </div>\n <div class="stat">\n <span class="stat-label">${t} Only</span>\n <span class="stat-value">${n.provider2Only}</span>\n </div>\n </div>\n <div class="comparison-actions">\n <button id="close-compare" class="btn btn-secondary">\n <span>Close</span>\n <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\n <path d="M18 6L6 18M6 6l12 12"/>\n </svg>\n </button>\n </div>\n </div>\n \n <div class="comparison-filters">\n <label class="filter-option">\n <input type="radio" name="comparison-filter" value="all" checked>\n <span>All Services</span>\n </label>\n <label class="filter-option">\n <input type="radio" name="comparison-filter" value="both">\n <span>Supported by Both</span>\n </label>\n <label class="filter-option">\n <input type="radio" name="comparison-filter" value="different">\n <span>Different Support</span>\n </label>\n </div>\n\n <div class="table-wrapper">\n <table id="compare-table" class="comparison-table" aria-label="Provider Comparison Table">\n <thead>\n <tr>\n <th>Service Name</th>\n <th class="provider-header ${e.toLowerCase()}">${e}</th>\n <th class="provider-header ${t.toLowerCase()}">${t}</th>\n <th>Status</th>\n </tr>\n </thead>\n <tbody>\n ${this._generateComparisonRows(e,t)}\n </tbody>\n </table>\n </div>\n `,this.container.style.display="block",this._setupComparisonFeatures(),requestAnimationFrame(()=>{this.container.classList.add("comparison-visible")})}_generateComparisonRows(e,t){return Object.entries(this.data).map(([n,a])=>{const s="yes"===a[e],r="yes"===a[t];let i="",o="";return s&&r?(i="both-supported",o="Both"):s?(i="provider1-only",o=`${e} only`):r?(i="provider2-only",o=`${t} only`):(i="neither-supported",o="Neither"),`\n <tr class="comparison-row ${i}" data-status="${i}">\n <td class="service-name">${n}</td>\n <td class="support-status ${s?"supported":"not-supported"}">\n <span class="status-indicator" aria-label="${s?"Supported":"Not supported"}">\n ${s?'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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 width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>'}\n </span>\n </td>\n <td class="support-status ${r?"supported":"not-supported"}">\n <span class="status-indicator" aria-label="${r?"Supported":"Not supported"}">\n ${r?'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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 width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>'}\n </span>\n </td>\n <td class="status-text">\n <span class="status-badge ${i}">${o}</span>\n </td>\n </tr>\n `}).join("")}_calculateComparisonStats(e,t){let n=0,a=0,s=0;return Object.values(this.data).forEach(r=>{const i="yes"===r[e],o="yes"===r[t];i&&o?n++:i?a++:o&&s++}),{shared:n,provider1Only:a,provider2Only:s}}_setupComparisonFeatures(){this.container.querySelector("#close-compare")?.addEventListener("click",()=>{this._closeComparison()}),this.container.querySelectorAll('input[name="comparison-filter"]').forEach(e=>{e.addEventListener("change",e=>{this._filterComparison(e.target.value)})})}_filterComparison(e){this.container.querySelectorAll(".comparison-row").forEach(t=>{const n=t.getAttribute("data-status");let a=!0;switch(e){case"both":a="both-supported"===n;break;case"different":a="provider1-only"===n||"provider2-only"===n;break;default:a=!0}t.style.display=a?"":"none"});const t=this.container.querySelectorAll('.comparison-row:not([style*="display: none"])').length;let n=this.container.querySelector(".filter-results");n||(n=document.createElement("div"),n.className="filter-results",this.container.querySelector(".table-wrapper").insertAdjacentElement("beforebegin",n)),n.textContent=`Showing ${t} services`}_closeComparison(){this.container.classList.add("comparison-hiding"),setTimeout(()=>{this.container.style.display="none",this.container.classList.remove("comparison-visible","comparison-hiding"),this.isComparing=!1,this._showEmptyState()},300)}_showEmptyState(){this.container.innerHTML='\n <div class="empty-state">\n <div class="empty-state-icon">⚖️</div>\n <h3>Ready to Compare</h3>\n <p>Select two services above to see a detailed comparison</p>\n </div>\n ',this.container.style.display="block"}_showSameProviderWarning(){this.container.innerHTML='\n <div class="warning-state">\n <div class="warning-icon">⚠️</div>\n <h3>Same Service Selected</h3>\n <p>Please select two different services to compare</p>\n </div>\n ',this.container.style.display="block"}_setupEventListeners(){[this.select1,this.select2].forEach(e=>{e?.addEventListener("change",()=>{this._generateCompareTable()})})}_setupKeyboardShortcuts(){document.addEventListener("keydown",e=>{this.isComparing&&"Escape"===e.key&&this._closeComparison()})}_trackComparison(e,t){"undefined"!=typeof gtag&&gtag("event","comparison_generated",{provider_1:e,provider_2:t})}}class PricingManager{constructor(e){this.container=document.querySelector(`#${e}-table-container`),this.currentData=null}generatePricingTable(e={}){if(!e.plans?.length)return void(this.container.innerHTML='<div class="error-state"><p>Error: Invalid pricing data structure.</p></div>');this.currentData=e;const t=Object.keys(e.plans[0]).filter(e=>"name"!==e);this.container.innerHTML=`\n <div class="table-wrapper">\n <table id="pricing-table" class="pricing-table" aria-label="Service Pricing Table">\n <thead>\n <tr>\n <th>Plans</th>\n ${t.map(e=>`\n <th class="service-column">\n <div class="service-header">\n <span class="service-name">${e}</span>\n </div>\n </th>\n `).join("")}\n </tr>\n </thead>\n <tbody>\n ${e.plans.map((e,n)=>`\n <tr class="pricing-row ${n%2==0?"even":"odd"}">\n <td class="plan-name">\n <strong>${e.name}</strong>\n </td>\n ${t.map(t=>{const n=e[t],a=!isNaN(parseFloat(n));return`\n <td class="price-cell ${a?"numeric-price":"text-price"}">\n <div class="price-content">\n <span class="price-value">${n}</span>\n ${a?'<span class="price-period">/month</span>':""}\n </div>\n </td>\n `}).join("")}\n </tr>\n `).join("")}\n </tbody>\n </table>\n </div>\n `,this._setupPricingFeatures()}_setupPricingFeatures(){this.container.querySelectorAll(".price-cell").forEach(e=>{e.addEventListener("mouseenter",()=>{const t=Array.from(e.parentNode.children).indexOf(e);this._highlightColumn(t)}),e.addEventListener("mouseleave",()=>{this._removeColumnHighlight()})})}_highlightColumn(e){this.container.querySelector("table").querySelectorAll(`td:nth-child(${e+1}), th:nth-child(${e+1})`).forEach(e=>e.classList.add("highlighted"))}_removeColumnHighlight(){this.container.querySelectorAll(".highlighted").forEach(e=>e.classList.remove("highlighted"))}}class PerformanceMonitor{constructor(){this.metrics={loadStart:performance.now(),domReady:null,fullyLoaded:null}}markDOMReady(){this.metrics.domReady=performance.now(),console.log(`DOM Ready: ${(this.metrics.domReady-this.metrics.loadStart).toFixed(2)}ms`)}markFullyLoaded(){this.metrics.fullyLoaded=performance.now(),console.log(`Fully Loaded: ${(this.metrics.fullyLoaded-this.metrics.loadStart).toFixed(2)}ms`)}getMetrics(){return{...this.metrics,totalLoadTime:this.metrics.fullyLoaded-this.metrics.loadStart,domLoadTime:this.metrics.domReady-this.metrics.loadStart}}}const loadingManager=new LoadingManager,performanceMonitor=new PerformanceMonitor;function setupURLComparison(){const e=new URLSearchParams(window.location.search),t=e.get("compare"),n=e.get("with");t&&n&&setTimeout(()=>{const e=document.querySelector("#provider1"),a=document.querySelector("#provider2");e&&a&&(e.value=t,a.value=n,e.dispatchEvent(new Event("change")))},1e3)}function setupScrollAnimations(){const e=document.querySelectorAll(".section, .card, .quick-nav-card");Utils.animateOnScroll(e)}function setupOfflineDetection(){window.addEventListener("online",()=>{console.log("Connection restored")}),window.addEventListener("offline",()=>{console.log("You are offline. Some features may not work.")})}document.addEventListener("DOMContentLoaded",async()=>{performanceMonitor.markDOMReady();try{const e=new TableManager("file-hosts-table-container","host-search-input","clear-host-search",{sortable:!0}),t=new TableManager("adult-hosts-table-container","adult-host-search-input","clear-adult-host-search",{sortable:!0}),n=new PricingManager("pricing"),a=loadingManager.show("#file-hosts-table-container","Loading file hosts..."),s=loadingManager.show("#adult-hosts-table-container","Loading adult hosts..."),r=loadingManager.show("#pricing-table-container","Loading pricing data..."),i=async(e,t=3)=>{for(let n=0;n<t;n++)try{const t=await fetch(e);if(!t.ok)throw new Error(`HTTP ${t.status}`);return await t.json()}catch(e){if(n===t-1)throw e;await new Promise(e=>setTimeout(e,1e3*(n+1)))}},[o,l,c]=await Promise.allSettled([i("./json/file-hosts.json"),i("./json/adult-hosts.json"),i("./json/pricing.json")]);"fulfilled"===o.status?(loadingManager.hide("#file-hosts-table-container",a),e.generateTable(o.value),new ComparisonManager("compare-table-container","provider1","provider2",o.value)):(loadingManager.hide("#file-hosts-table-container",a),console.error("File hosts error:",o.reason)),"fulfilled"===l.status?(loadingManager.hide("#adult-hosts-table-container",s),t.generateTable(l.value)):(loadingManager.hide("#adult-hosts-table-container",s),console.error("Adult hosts error:",l.reason)),"fulfilled"===c.status?(loadingManager.hide("#pricing-table-container",r),n.generatePricingTable(c.value)):(loadingManager.hide("#pricing-table-container",r),console.error("Pricing error:",c.reason)),setupURLComparison(),setupScrollAnimations(),setupOfflineDetection(),performanceMonitor.markFullyLoaded()}catch(e){console.error("Critical error initializing application:",e),loadingManager.hideAll()}}),window.addEventListener("error",e=>{console.error("Global error:",e.error)}),window.addEventListener("unhandledrejection",e=>{console.error("Unhandled promise rejection:",e.reason),e.preventDefault()});