debrid-services-comparison/dist/js/app-min.js
copilot-swe-agent[bot] f8f927e8de Complete modern redesign: responsive, accessible, feature-rich comparison tool
Co-authored-by: fynks <75840152+fynks@users.noreply.github.com>
2025-08-19 04:08:10 +00:00

1 line
No EOL
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const Utils={debounce:(e,t=300,s=!1)=>{let n;return function(...i){const a=s&&!n;clearTimeout(n),n=setTimeout(()=>{n=null,s||e.apply(this,i)},t),a&&e.apply(this,i)}},throttle:(e,t)=>{let s;return function(){const n=arguments,i=this;s||(e.apply(i,n),s=!0,setTimeout(()=>s=!1,t))}},animateOnScroll:(e,t={})=>{const s=new IntersectionObserver(e=>{e.forEach(e=>{e.isIntersecting&&(e.target.classList.add("animate-in"),s.unobserve(e.target))})},{threshold:.1,rootMargin:"0px 0px -50px 0px",...t});e.forEach(e=>s.observe(e))}};class LoadingManager{constructor(){this.activeLoaders=new Set}show(e,t="Loading..."){const s=Date.now().toString(),n=document.createElement("div");return n.className="loading-overlay",n.setAttribute("data-loader-id",s),n.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(n),this.activeLoaders.add(s),requestAnimationFrame(()=>{n.classList.add("loading-overlay--visible")}),s}hide(e,t){"string"==typeof e&&(e=document.querySelector(e));const s=e.querySelector(`[data-loader-id="${t}"]`);s&&(s.classList.add("loading-overlay--hiding"),this.activeLoaders.delete(t),setTimeout(()=>{s.parentNode&&e.removeChild(s)},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,s,n={}){this.container=document.querySelector(`#${e}`),this.searchInput=document.querySelector(`#${t}`),this.clearIcon=document.querySelector(`#${s}`),this.options={sortable:!0,filterable:!0,pagination:!1,itemsPerPage:50,...n},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]]),s=this.container.id.replace("-container","");this.container.innerHTML=`\n <div class="table-wrapper">\n <table id="${s}" 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,s])=>`\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="${s[e]}">\n <span class="status-indicator ${"yes"===s[e]?"supported":"not-supported"}" \n aria-label="${"yes"===s[e]?"Supported":"Not supported"}">\n ${"yes"===s[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,s])=>{t.toLowerCase().includes(e)&&(this.filteredData[t]=s)}),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,s=Object.keys(this.currentData).length;let n=this.container.querySelector(".search-results");n||(n=document.createElement("div"),n.className="search-results",this.container.insertBefore(n,this.container.querySelector(".table-wrapper"))),e?(n.textContent=`Showing ${t} of ${s} services`,n.style.display="block"):n.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"),s="asc"===(this.currentSort.column===t?this.currentSort.direction:"asc")?"desc":"asc";this.currentSort={column:t,direction:s},this.container.querySelectorAll("th.sortable").forEach(e=>{e.setAttribute("aria-sort","none"),e.classList.remove("sorted-asc","sorted-desc")}),e.setAttribute("aria-sort","asc"===s?"ascending":"descending"),e.classList.add(`sorted-${s}`),this._sortData(),this._updateTableContent()}_sortData(){const{column:e,direction:t}=this.currentSort,s=Object.entries(this.filteredData);s.sort(([s,n],[i,a])=>{let r,o;return"service"===e?(r=s.toLowerCase(),o=i.toLowerCase()):(r="yes"===n[e]?1:0,o="yes"===a[e]?1:0),"asc"===t?r>o?1:r<o?-1:0:r<o?1:r>o?-1:0}),this.filteredData=Object.fromEntries(s)}_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,s,n){this.container=document.querySelector(`#${e}`),this.select1=document.querySelector(`#${t}`),this.select2=document.querySelector(`#${s}`),this.data=n,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,s)=>{t.innerHTML='<option value="">Loading services...</option>',setTimeout(()=>{t.innerHTML='<option value="">Choose a service...</option>',e.forEach(e=>{const s=document.createElement("option");s.value=e,s.textContent=e,t.appendChild(s)})},100*(s+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 s=loadingManager.show(this.container,"Generating comparison...");setTimeout(()=>{this._renderComparisonTable(e,t),loadingManager.hide(this.container,s),this._trackComparison(e,t)},500)}_renderComparisonTable(e,t){const s=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">${s.shared}</span>\n </div>\n <div class="stat">\n <span class="stat-label">${e} Only</span>\n <span class="stat-value">${s.provider1Only}</span>\n </div>\n <div class="stat">\n <span class="stat-label">${t} Only</span>\n <span class="stat-value">${s.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(([s,n])=>{const i="yes"===n[e],a="yes"===n[t];let r="",o="";return i&&a?(r="both-supported",o="Both"):i?(r="provider1-only",o=`${e} only`):a?(r="provider2-only",o=`${t} only`):(r="neither-supported",o="Neither"),`\n <tr class="comparison-row ${r}" data-status="${r}">\n <td class="service-name">${s}</td>\n <td class="support-status ${i?"supported":"not-supported"}">\n <span class="status-indicator" aria-label="${i?"Supported":"Not supported"}">\n ${i?'<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 ${a?"supported":"not-supported"}">\n <span class="status-indicator" aria-label="${a?"Supported":"Not supported"}">\n ${a?'<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 ${r}">${o}</span>\n </td>\n </tr>\n `}).join("")}_calculateComparisonStats(e,t){let s=0,n=0,i=0;return Object.values(this.data).forEach(a=>{const r="yes"===a[e],o="yes"===a[t];r&&o?s++:r?n++:o&&i++}),{shared:s,provider1Only:n,provider2Only:i}}_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 s=t.getAttribute("data-status");let n=!0;switch(e){case"both":n="both-supported"===s;break;case"different":n="provider1-only"===s||"provider2-only"===s;break;default:n=!0}t.style.display=n?"":"none"});const t=this.container.querySelectorAll('.comparison-row:not([style*="display: none"])').length;let s=this.container.querySelector(".filter-results");s||(s=document.createElement("div"),s.className="filter-results",this.container.querySelector(".table-wrapper").insertAdjacentElement("beforebegin",s)),s.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,s)=>`\n <tr class="pricing-row ${s%2==0?"even":"odd"}">\n <td class="plan-name">\n <strong>${e.name}</strong>\n </td>\n ${t.map(t=>{const s=e[t],n=!isNaN(parseFloat(s));return`\n <td class="price-cell ${n?"numeric-price":"text-price"}">\n <div class="price-content">\n <span class="price-value">${s}</span>\n ${n?'<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"),s=e.get("with");t&&s&&setTimeout(()=>{const e=document.querySelector("#provider1"),n=document.querySelector("#provider2");e&&n&&(e.value=t,n.value=s,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}),s=new PricingManager("pricing"),n=loadingManager.show("#file-hosts-table-container","Loading file hosts..."),i=loadingManager.show("#adult-hosts-table-container","Loading adult hosts..."),a=loadingManager.show("#pricing-table-container","Loading pricing data..."),r=async(e,t=3)=>{for(let s=0;s<t;s++)try{const t=await fetch(e);if(!t.ok)throw new Error(`HTTP ${t.status}`);return await t.json()}catch(e){if(s===t-1)throw e;await new Promise(e=>setTimeout(e,1e3*(s+1)))}},[o,l,c]=await Promise.allSettled([r("./json/file-hosts.json"),r("./json/adult-hosts.json"),r("./json/pricing.json")]);"fulfilled"===o.status?(loadingManager.hide("#file-hosts-table-container",n),e.generateTable(o.value),new ComparisonManager("compare-table-container","provider1","provider2",o.value)):(loadingManager.hide("#file-hosts-table-container",n),console.error("File hosts error:",o.reason)),"fulfilled"===l.status?(loadingManager.hide("#adult-hosts-table-container",i),t.generateTable(l.value)):(loadingManager.hide("#adult-hosts-table-container",i),console.error("Adult hosts error:",l.reason)),"fulfilled"===c.status?(loadingManager.hide("#pricing-table-container",a),s.generatePricingTable(c.value)):(loadingManager.hide("#pricing-table-container",a),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()});class TouchGestureManager{constructor(){this.touchStart=null,this.touchEnd=null,this.minSwipeDistance=100,this.maxSwipeTime=300,this.setupTouchListeners()}setupTouchListeners(){document.addEventListener("touchstart",e=>{this.touchStart={x:e.changedTouches[0].screenX,y:e.changedTouches[0].screenY,time:Date.now()}},{passive:!0}),document.addEventListener("touchend",e=>{this.touchStart&&(this.touchEnd={x:e.changedTouches[0].screenX,y:e.changedTouches[0].screenY,time:Date.now()},this.handleSwipe())},{passive:!0})}handleSwipe(){if(!this.touchStart||!this.touchEnd)return;const e=this.touchEnd.x-this.touchStart.x,t=this.touchEnd.y-this.touchStart.y;if(this.touchEnd.time-this.touchStart.time>this.maxSwipeTime)return;Math.sqrt(e*e+t*t)<this.minSwipeDistance||(Math.abs(e)>Math.abs(t)?e>0?this.dispatchSwipeEvent("swiperight"):this.dispatchSwipeEvent("swipeleft"):t>0?this.dispatchSwipeEvent("swipedown"):this.dispatchSwipeEvent("swipeup"))}dispatchSwipeEvent(e){document.dispatchEvent(new CustomEvent(e,{detail:{startPos:this.touchStart,endPos:this.touchEnd}}))}}class VirtualScrollManager{constructor(e,t=50){this.container=e,this.itemHeight=t,this.viewportHeight=e.clientHeight,this.totalItems=0,this.visibleStart=0,this.visibleEnd=0,this.scrollContent=null,this.renderCallback=null}init(e,t){this.totalItems=e.length,this.renderCallback=t,this.setupVirtualScroll(),this.updateVisibleRange()}setupVirtualScroll(){this.container.innerHTML=`\n <div class="virtual-scroll-content" style="height: ${this.totalItems*this.itemHeight}px;">\n <div class="virtual-scroll-viewport"></div>\n </div>\n `,this.scrollContent=this.container.querySelector(".virtual-scroll-viewport"),this.container.addEventListener("scroll",Utils.throttle(()=>{this.updateVisibleRange()},16))}updateVisibleRange(){const e=this.container.scrollTop,t=Math.ceil(this.viewportHeight/this.itemHeight);this.visibleStart=Math.max(0,Math.floor(e/this.itemHeight)-t),this.visibleEnd=Math.min(this.totalItems,this.visibleStart+3*t),this.renderVisibleItems()}renderVisibleItems(){if(!this.renderCallback)return;const e=document.createDocumentFragment();for(let t=this.visibleStart;t<this.visibleEnd;t++){const s=this.renderCallback(t);s.style.position="absolute",s.style.top=t*this.itemHeight+"px",s.style.height=`${this.itemHeight}px`,s.style.width="100%",e.appendChild(s)}this.scrollContent.innerHTML="",this.scrollContent.appendChild(e)}}class SearchManager{constructor(e,t){this.input=e,this.suggestionsContainer=t,this.currentIndex=-1,this.suggestions=[],this.setupEventListeners()}setupEventListeners(){this.input.addEventListener("input",Utils.debounce(e=>{this.handleInput(e.target.value)},300)),this.input.addEventListener("keydown",e=>{this.handleKeyDown(e)}),this.input.addEventListener("blur",()=>{setTimeout(()=>this.hideSuggestions(),200)}),document.addEventListener("click",e=>{this.input.contains(e.target)||this.suggestionsContainer.contains(e.target)||this.hideSuggestions()})}async handleInput(e){if(e.length<2)this.hideSuggestions();else try{this.suggestions=await this.fetchSuggestions(e),this.renderSuggestions()}catch(e){console.error("Error fetching suggestions:",e)}}async fetchSuggestions(e){return Object.keys(window.fileHostsData||{}).filter(t=>t.toLowerCase().includes(e.toLowerCase())).slice(0,5)}renderSuggestions(){0!==this.suggestions.length?(this.suggestionsContainer.innerHTML=this.suggestions.map((e,t)=>`\n <div class="search-suggestion" data-index="${t}">\n ${this.highlightMatch(e,this.input.value)}\n </div>\n `).join(""),this.suggestionsContainer.classList.add("visible"),this.currentIndex=-1,this.suggestionsContainer.querySelectorAll(".search-suggestion").forEach((e,t)=>{e.addEventListener("click",()=>{this.selectSuggestion(t)})})):this.hideSuggestions()}highlightMatch(e,t){const s=new RegExp(`(${t})`,"gi");return e.replace(s,"<strong>$1</strong>")}handleKeyDown(e){if(this.suggestionsContainer.classList.contains("visible"))switch(e.key){case"ArrowDown":e.preventDefault(),this.navigateSuggestions(1);break;case"ArrowUp":e.preventDefault(),this.navigateSuggestions(-1);break;case"Enter":e.preventDefault(),this.currentIndex>=0&&this.selectSuggestion(this.currentIndex);break;case"Escape":this.hideSuggestions()}}navigateSuggestions(e){const t=this.suggestionsContainer.querySelectorAll(".search-suggestion");this.currentIndex>=0&&t[this.currentIndex].classList.remove("highlighted"),this.currentIndex+=e,this.currentIndex<0&&(this.currentIndex=t.length-1),this.currentIndex>=t.length&&(this.currentIndex=0),t[this.currentIndex].classList.add("highlighted"),t[this.currentIndex].scrollIntoView({block:"nearest"})}selectSuggestion(e){this.input.value=this.suggestions[e],this.hideSuggestions(),this.input.dispatchEvent(new Event("input"))}hideSuggestions(){this.suggestionsContainer.classList.remove("visible"),this.currentIndex=-1}}class PullToRefreshManager{constructor(e,t){this.container=e,this.callback=t,this.isRefreshing=!1,this.startY=0,this.currentY=0,this.threshold=80,this.setupPullToRefresh()}setupPullToRefresh(){this.container.classList.add("pull-to-refresh");const e=document.createElement("div");e.className="pull-to-refresh-indicator",e.innerHTML='<div class="loading-spinner"></div>',this.container.appendChild(e),this.container.addEventListener("touchstart",e=>{0!==this.container.scrollTop||this.isRefreshing||(this.startY=e.touches[0].clientY)},{passive:!0}),this.container.addEventListener("touchmove",e=>{if(this.startY&&!this.isRefreshing){this.currentY=e.touches[0].clientY;const t=this.currentY-this.startY;if(t>0&&0===this.container.scrollTop){e.preventDefault();Math.min(t/this.threshold,1);t>this.threshold&&this.container.classList.add("pulling")}}},{passive:!1}),this.container.addEventListener("touchend",()=>{if(this.startY&&this.currentY&&!this.isRefreshing){this.currentY-this.startY>this.threshold&&this.triggerRefresh()}this.container.classList.remove("pulling"),this.startY=0,this.currentY=0},{passive:!0})}async triggerRefresh(){if(!this.isRefreshing){this.isRefreshing=!0;try{await this.callback()}catch(e){console.error("Refresh failed:",e)}finally{this.isRefreshing=!1}}}}class ToastManager{constructor(){this.container=this.createContainer(),this.toasts=new Map}createContainer(){let e=document.querySelector(".toast-container");return e||(e=document.createElement("div"),e.className="toast-container",document.body.appendChild(e)),e}show(e,t="info",s=3e3){const n=Date.now().toString(),i=this.createToast(e,t,n);return this.container.appendChild(i),this.toasts.set(n,i),s>0&&setTimeout(()=>{this.remove(n)},s),n}createToast(e,t,s){const n=document.createElement("div");n.className=`toast ${t}`,n.setAttribute("data-toast-id",s),n.innerHTML=`\n <div class="toast-content">\n <span class="toast-message">${e}</span>\n <button class="toast-close" aria-label="Close notification">×</button>\n </div>\n `;return n.querySelector(".toast-close").addEventListener("click",()=>{this.remove(s)}),n}remove(e){const t=this.toasts.get(e);t&&(t.style.animation="slideOutRight 0.3s ease forwards",setTimeout(()=>{t.parentNode&&this.container.removeChild(t),this.toasts.delete(e)},300))}success(e,t=3e3){return this.show(e,"success",t)}error(e,t=5e3){return this.show(e,"error",t)}warning(e,t=4e3){return this.show(e,"warning",t)}info(e,t=3e3){return this.show(e,"info",t)}}class MobileNavManager{constructor(){this.setupMobileNav()}setupMobileNav(){window.innerWidth<=768&&this.createMobileNav(),window.addEventListener("resize",Utils.debounce(()=>{window.innerWidth<=768?this.createMobileNav():this.removeMobileNav()},300))}createMobileNav(){if(document.querySelector(".mobile-nav-toggle"))return;const e=document.createElement("button");e.className="mobile-nav-toggle",e.innerHTML="☰",e.setAttribute("aria-label","Toggle mobile navigation");const t=document.createElement("div");t.className="mobile-nav-menu",t.innerHTML='\n <a href="#pricing" class="mobile-nav-item">Pricing</a>\n <a href="#file-hosts" class="mobile-nav-item">File Hosts</a>\n <a href="#compare" class="mobile-nav-item">Compare</a>\n <a href="#adult-hosts" class="mobile-nav-item">Adult Hosts</a>\n <a href="#live-status" class="mobile-nav-item">Status</a>\n ',document.body.appendChild(e),document.body.appendChild(t),e.addEventListener("click",()=>{t.classList.toggle("visible")}),document.addEventListener("click",s=>{e.contains(s.target)||t.contains(s.target)||t.classList.remove("visible")}),t.querySelectorAll(".mobile-nav-item").forEach(e=>{e.addEventListener("click",()=>{t.classList.remove("visible")})})}removeMobileNav(){const e=document.querySelector(".mobile-nav-toggle"),t=document.querySelector(".mobile-nav-menu");e&&e.remove(),t&&t.remove()}}class ProgressiveEnhancementManager{constructor(){this.features=new Map,this.detectFeatures()}detectFeatures(){this.features.set("intersectionObserver","IntersectionObserver"in window),this.features.set("webp",this.supportsWebP()),this.features.set("serviceWorker","serviceWorker"in navigator),this.features.set("touchEvents","ontouchstart"in window),this.features.set("pointerEvents","PointerEvent"in window),this.features.set("containerQueries",CSS.supports("container-type: inline-size"))}async supportsWebP(){return new Promise(e=>{const t=new Image;t.onload=t.onerror=()=>{e(2===t.height)},t.src="data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA"})}hasFeature(e){return this.features.get(e)||!1}enhanceIfSupported(e,t){this.hasFeature(e)&&t()}}const touchGestureManager=new TouchGestureManager,toastManager=new ToastManager,mobileNavManager=new MobileNavManager,progressiveEnhancementManager=new ProgressiveEnhancementManager;if(document.addEventListener("DOMContentLoaded",()=>{const e=document.querySelector("#file-host-search"),t=document.createElement("div");t.className="search-suggestions",e&&(e.parentNode.classList.add("search-container"),e.parentNode.appendChild(t),new SearchManager(e,t));const s=document.querySelector("#adult-host-search"),n=document.createElement("div");if(n.className="search-suggestions",s&&(s.parentNode.classList.add("search-container"),s.parentNode.appendChild(n),new SearchManager(s,n)),progressiveEnhancementManager.hasFeature("touchEvents")){const e=document.querySelector("main");e&&new PullToRefreshManager(e,async()=>{toastManager.info("Refreshing data..."),await new Promise(e=>setTimeout(e,1e3)),window.location.reload()})}const i=document.createElement("div");i.className="scroll-indicator",i.innerHTML='<div class="scroll-indicator-bar"></div>',document.body.appendChild(i);window.addEventListener("scroll",Utils.throttle(()=>{const e=(document.body.scrollTop||document.documentElement.scrollTop)/(document.documentElement.scrollHeight-document.documentElement.clientHeight)*100;i.querySelector(".scroll-indicator-bar").style.width=e+"%"},16))}),document.addEventListener("swipeleft",e=>{console.log("Swiped left")}),document.addEventListener("swiperight",e=>{console.log("Swiped right")}),progressiveEnhancementManager.hasFeature("serviceWorker")&&navigator.serviceWorker.register("/sw.js").then(e=>{console.log("ServiceWorker registration successful"),e.addEventListener("updatefound",()=>{const t=e.installing;t.addEventListener("statechange",()=>{"installed"===t.state&&navigator.serviceWorker.controller&&toastManager.info("New version available! Refresh to update.",0)})})}).catch(e=>{console.log("ServiceWorker registration failed:",e)}),"PerformanceObserver"in window){const e=new PerformanceObserver(e=>{for(const t of e.getEntries())"largest-contentful-paint"===t.entryType&&console.log(`LCP: ${t.startTime}ms`),"first-input"===t.entryType&&console.log(`FID: ${t.processingStart-t.startTime}ms`)});try{e.observe({entryTypes:["largest-contentful-paint","first-input"]})}catch(e){console.log("Performance observation not supported")}}