1<!DOCTYPE html> 2<html lang="en"> 3 4<head>
vendor: 75 bytes, lines 4-7
4 5 6 7 <script async src="https://www.googletagmanager.com/gtag/js?id=
7G-7S01FXEE6H
vendor: 12 bytes, line 7
7"></script>
8<script>
vendor: 129 bytes, lines 9-12
9window.dataLayer = window.dataLayer || []; 10function gtag() { dataLayer.push(arguments);} 11gtag("js", new Date()); 12gtag("config", "
12G-7S01FXEE6H
vendor: 4 bytes, line 12
12");
13</script>
13 14 15 16 17 <meta charset="utf-8" /> 18 <title>Shop</title> 19 <meta name="viewport" content="width=device-width, initial-scale=1" /> 20 <meta name="keywords" content="" /> 21 <meta name="description" content="" /> 22 <!-- Icons --> 23 <meta name="msapplication-TileColor" content="#1bb8de" /> 24 <meta name="msapplication-TileImage" content="/assets/branding/shop.runningcompany.com/ms-icon-144x144.png" /> 25 <meta name="theme-color" content="#8dc63f" /> 26 <meta property="og:title" content="Shop Naperville Running" /> 27 <meta property="og:image" content="https://shop.runningcompany.com/assets/branding/shop.runningcompany.com/logo.png" /> 28 <meta property="og:description" content="" /> 29 <link rel="canonical" href="https://shop.runningcompany.com/shop" /> 30 <link rel="shortcut icon" id="appFavicon" href="/assets/branding/shop.runningcompany.com/favicon.ico" /> 31 <link rel="apple-touch-icon" id="appFaviconApple57" sizes="57x57" href="/assets/branding/shop.runningcompany.com/apple-icon-57x57.png" /> 32 <link rel="apple-touch-icon" id="appFaviconApple60" sizes="60x60" href="/assets/branding/shop.runningcompany.com/apple-icon-60x60.png" /> 33 <link rel="apple-touch-icon" id="appFaviconApple72" sizes="72x72" href="/assets/branding/shop.runningcompany.com/apple-icon-72x72.png" /> 34 <link rel="apple-touch-icon" id="appFaviconApple76" sizes="76x76" href="/assets/branding/shop.runningcompany.com/apple-icon-76x76.png" /> 35 <link rel="apple-touch-icon" id="appFaviconApple114" sizes="114x114" href="/assets/branding/shop.runningcompany.com/apple-icon-114x114.png" /> 36 <link rel="apple-touch-icon" id="appFaviconApple120" sizes="120x120" href="/assets/branding/shop.runningcompany.com/apple-icon-120x120.png" /> 37 <link rel="apple-touch-icon" id="appFaviconApple144" sizes="144x144" href="/assets/branding/shop.runningcompany.com/apple-icon-144x144.png" /> 38 <link rel="apple-touch-icon" id="appFaviconApple152" sizes="152x152" href="/assets/branding/shop.runningcompany.com/apple-icon-152x152.png" /> 39 <link rel="apple-touch-icon" id="appFaviconApple180" sizes="180x180" href="/assets/branding/shop.runningcompany.com/apple-icon-180x180.png" /> 40 <link rel="icon" type="image/png" id="appFavicon192" sizes="192x192" href="/assets/branding/shop.runningcompany.com/android-icon-192x192.png" /> 41 <link rel="icon" type="image/png" id="appFavicon32" sizes="32x32" href="/assets/branding/shop.runningcompany.com/favicon-32x32.png" /> 42 <link rel="icon" type="image/png" id="appFavicon96" sizes="96x96" href="/assets/branding/shop.runningcompany.com/favicon-96x96.png" /> 43 <link rel="icon" type="image/png" id="appFavicon16" sizes="16x16" href="/assets/branding/shop.runningcompany.com/favicon-16x16.png" /> 44 <!-- Font Preloading - Optimized for Performance --> 45 <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin> 46 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 47 <link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700;800&display=swap"> 48 <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700;800&display=swap" media="print" onload="this.media='all'"> 49 <noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700;800&display=swap"></noscript> 50 <!-- Styles --> 51 <link href="/assets/styles/font-variables.css?v=1.2.0" rel="stylesheet" type="text/css" media="all" /> 52 <style>:root { --font-primary: "Nunito", "Open Sans", "Helvetica Neue", sans-serif; }</style> 53 <link href="/assets/admin/styles/spectrum.css?v=1.2.0" rel="stylesheet" type="text/css" media="all" /> 54 <link href="/assets/styles/bootstrap.css?v=1.2.0" rel="stylesheet" type="text/css" media="all"/> 55 <link href="/assets/styles/themify-icons.css?v=1.2.0" rel="stylesheet" type="text/css" media="all" /> 56 <link href="/assets/styles/flexslider.css?v=1.2.0" rel="stylesheet" type="text/css" media="all" /> 57 <link href="/assets/styles/theme.css?v=1.2.0" rel="stylesheet" type="text/css" media="all" /> 58 <link href="/assets/styles/custom.css?v=1.2.0" rel="stylesheet" type="text/css" media="all" /> 59 <link href="/assets/styles/runfree.css?v=1.2.0" rel="stylesheet" type="text/css" media="all" /> 60 <link href="/assets/styles/modal.css?v=1.2.0" rel="stylesheet" type="text/css" /> 61 <link href="/assets/styles/datetimepicker.css?v=1.2.0" rel="stylesheet" type="text/css" media="all" /> 62 <link href="/assets/styles/font-awesome.min.css?v=1.2.0" rel="stylesheet" type="text/css" media="all" /> 63 <link rel="stylesheet" href="/assets/styles/tooltips.css?v=1.2.0" rel="stylesheet" type="text/css" media="all" /> 64 <link href="/assets/styles/runfreefont.css?v=1.2.0" rel="stylesheet" type="text/css" media="all" /> 65 <link href="/assets/styles/search-overlay.css?v=1.2.0" rel="stylesheet" type="text/css" media="all" /> 66 <link href="/assets/branding/shop.runningcompany.com/branding.css?v=1.2.0" rel="stylesheet" type="text/css" media="all" /> 67
67<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
67 68
68<script> 69 // Universal tracking helper - supports both GTM and direct gtag 70 window.dataLayer = window.dataLayer || []; 71 function trackEvent(eventName, eventData) { 72 // Clear previous ecommerce object (GTM best practice) 73 dataLayer.push({ ecommerce: null }); 74 75 // Map UA field names to GA4 schema (keep both for store GTM compatibility) 76 if (eventData && eventData.items) { 77 eventData.items = eventData.items.map(function(item) { 78 var mapped = { 79 item_id: item.item_id || item.id, 80 item_name: item.item_name || item.name, 81 item_brand: item.item_brand || item.brand, 82 item_category: item.item_category || item.category, 83 item_variant: item.item_variant || item.variant, 84 item_list_name: item.item_list_name || item.list_name, 85 index: item.index != null ? item.index : item.list_position, 86 quantity: item.quantity, 87 price: item.price, 88 id: item.id, 89 name: item.name, 90 brand: item.brand, 91 category: item.category, 92 variant: item.variant, 93 list_name: item.list_name, 94 list_position: item.list_position 95 }; 96 if (item.affiliation) mapped.affiliation = item.affiliation; 97 return mapped; 98 }); 99 } 100 101 // Map legacy UA event names to GA4 equivalents 102 var ga4EventName = eventName; 103 if (eventName === 'checkout_progress') { 104 ga4EventName = 'add_shipping_info'; 105 } 106 107 // GTM format: push object with event property (sends both legacy and GA4 names) 108 var gtmData = { event: eventName }; 109 if (eventData) { 110 if (eventData.items) gtmData.ecommerce = eventData; 111 for (var key in eventData) { 112 if (eventData.hasOwnProperty(key)) gtmData[key] = eventData[key]; 113 } 114 } 115 dataLayer.push(gtmData); 116 117 // Also push GA4 event name if different (for GTM GA4 triggers) 118 if (ga4EventName !== eventName) { 119 dataLayer.push({ ecommerce: null }); 120 var ga4GtmData = { event: ga4EventName }; 121 if (eventData) { 122 if (eventData.items) ga4GtmData.ecommerce = eventData; 123 for (var key in eventData) { 124 if (eventData.hasOwnProperty(key)) ga4GtmData[key] = eventData[key]; 125 } 126 } 127 dataLayer.push(ga4GtmData); 128 } 129 130 // Direct gtag format (for stores using GA4 directly) - always use GA4 event name 131 if (typeof gtag === 'function') { 132 gtag('event', ga4EventName, eventData); 133 } 134 } 135 </script>
135 136 137 <style> 138 :root { 139 --primary: #8dc63f; 140 --primaryOpaque: rgba(141,198,63,0.1); 141 --secondary: #fefffe; 142 --white: #FFF; 143 --almostblack: #252525; 144 --grey: #eff1f5; 145 --red: #ff104b; 146 --shadow: 0px 0px 8px 2px rgba(0, 0, 0, 0.08); 147 --bigShadow: 0px 12px 23px 0px rgba(0, 0, 0, .1); 148 --transition: 0.5s; 149 --even: #FCFCFC; 150 --siteLogo: url(/assets/branding/shop.runningcompany.com/logo.png); 151 --input: #F5F6FA; 152 --border: #f5f6fa; 153 --inputfocus: #e7ebf1; 154 --glass: rgba(250, 250, 250, 0.7); 155 --navutility: #f5f8fb; 156 --menu: #434c5e; 157 --filterhr: #d2d8e5; 158 --bggrey: #f5f6fa; 159 --thumbloader: linear-gradient(90deg, #eef1f5 0px, #fbfbfd 40px, #eef1f5 80px); 160 --skeleton-one: #eff1f5; 161 --skeleton-two: #F5F6FA; 162 --disappearing-header: linear-gradient(to bottom, rgba(245, 246, 250, 1) 0%, rgba(245, 246, 250, 0) 100%); 163 --disappearing-footer: linear-gradient(to bottom, rgba(245, 246, 250, 0) 0%, rgba(245, 246, 250, 1) 100%); 164 } 165 </style> 166 <link href="/assets/styles/round.css" rel="stylesheet" type="text/css" media="all" /> 167
167<script> 168 var logoCard = '/assets/branding/shop.runningcompany.com/logo.png'; 169 var logoUrl = '/'; 170 </script>
170 171 172
172<script>var cdnOn = false;</script>
vendor: 1 bytes, line 172
172
173<script type="text/javascript">var _learnq;</script>
173<script type="text/javascript">window.blueshift = null;</script>
173 174
174<script type="application/ld+json">{"@context":"https://schema.org","@type":"Organization","@id":"https://www.runningcompany.com/#organization","name":"Naperville Running","url":"https://www.runningcompany.com/","logo":"https://www.runningcompany.com/logo.png","sameAs":["https://shop.runningcompany.com"]}</script>
174 175 <!-- End Header Script --> 176</head> 177 178<body class="scroll-assist"> 179 180 181 <!-- Cart Template //--> 182 <li id="CartTemplate" class="cartItem template"> 183 <div class="productImage" style="background-image: url('{{product.url}}')"></div> 184 <div class="rel"> 185 <div class="productName">{{product.name}}</div> 186 <div class="productPrice"> 187 {{product.quantity}} at ${{product.cost}} - {{product.options}} 188 </div> 189 <div class="remove" data-index="{{index}}"></div> 190 </div> 191 </li> 192 193 <div class="nav-container"> 194 <nav> 195 <div class="nav-bar"> 196 <div class="module left"> 197 <img id="StoreLogo" class="logo logo-dark" alt="Naperville Running" src="/assets/branding/shop.runningcompany.com/logo.png" /> 198 </div> 199 200 <!-- Mobile toggle (hamburger) --> 201 <div class="module right widget-handle mobile-toggle visible-sm visible-xs"> 202 <i class="icon-Main-Menu mobilemenu"></i> 203 </div> 204 205 <!-- Cart icon - visible on mobile only --> 206 <div id="MobileCartMenu" class="module right widget-handle visible-sm visible-xs ecomtrue" style="width: 65px; padding: 0; border-left: 2px solid var(--border); text-align: center;"> 207 <a href="/cart" style="text-decoration: none; position: relative; display: block; height: 69px; filter: brightness(100%);"> 208 <i class="icon-ti-Cart" style="line-height: 69px !important; font-size: 24px; display: inline-block; vertical-align: middle;"></i> 209 <span id="MobileCartItemCount" class="label number"></span> 210 </a> 211 </div> 212 213 <div class="module-group right pt0 pb0"> 214 215 <!-- Account Pod --> 216 <div id="AccountMenu" class="module right widget-handle hidden-sm hidden-xs"> 217 <ul class="menu"> 218 <li class="has-dropdown-account" style="border-top: unset;"><i class="icon-Profile"></i> 219 <span class="title">My Account</span> 220 <ul style="margin-top: -7px; margin-right: -20px;"><li><a href="/login"><i class="icon-Log-In dropdown-icon"></i> Login</a></li><li><a href="/signup"><i class="icon-Signup dropdown-icon"></i> Signup</i></a></li></ul> 221 </li> 222 </ul> 223 </div> 224 225 <!-- Cart Pod --> 226 <div id="CartMenuSize" class="module widget-handle cart-widget-handle right ecomtrue hidden-sm hidden-xs"> 227 <div class="cart max" data-url="/cart"> 228 <i class="icon-ti-Cart"></i> 229 <span id="CartItemCount" class="label number"></span> 230 <span class="title">Shopping Cart</span> 231 </div> 232 <div id="CartMenu" class="function"> 233 <div class="widget" style="padding: 16px;"> 234 <h6 class="title">Shopping Cart</h6> 235 <hr /> 236 <ul id="CartInfo" class="cart-overview"></ul> 237 <div class="cart-controls mt16"> 238 <a class="btn btn-sm btn-filled" href="/cart">View Cart</a> 239 <div class="list-inline pull-right"> 240 <span class="cart-total">Total: </span> 241 <span id="CartTotal" class="number"></span> 242 </div> 243 </div> 244 </div> 245 </div> 246 </div> 247 248 <!-- Search Pod - Overlay Trigger --> 249 <div id="SearchOverlayTrigger" class="module widget-handle search-widget-handle right ecomtrue hidden-sm hidden-xs" style="cursor: pointer; -webkit-tap-highlight-color: transparent; touch-action: manipulation;"> 250 <div class="search max"> 251 <i class="icon-aisearch" style="pointer-events: none;"></i> 252 <span class="title" style="pointer-events: none;">Search Site</span> 253 </div> 254 </div> 255 256 <!-- Menu Pod --> 257 <div id="TheMenu" class="module left pt0 pb16"> 258 <ul id="MainMenu" class="menu"><li class="has-dropdown"><a href="#">Shop by Category</a><ul class="mega-menu"><li><ul id="HeaderCategories"></ul></li></ul></li><li id="GiftCardCart"><a href="/giftcard">Gift Cards</a></li><li style="display:none"></li></ul> 259 </div> 260 261 </div> 262 <!-- End Modal Group --> 263 264 </div> 265 </nav> 266 </div> 267 268 <!-- Search Overlay Component -->
269 <!-- RunFree Search Overlay - Modular Component --> 270<div id="SearchOverlay" class="search-overlay" role="dialog" aria-modal="true" aria-label="Search Products" aria-hidden="true"> 271 <div class="search-overlay-header"> 272 <div class="search-logo"> 273 <img id="SearchStoreLogo" alt="" src="" /> 274 </div> 275 <div class="search-input-container"> 276 <i class="icon-aisearch search-icon" aria-hidden="true"></i> 277 <input 278 type="search" 279 id="SearchOverlayInput" 280 class="search-overlay-input" 281 placeholder="Search for Products, Brands..." 282 autocomplete="off" 283 spellcheck="false" 284 aria-label="Search for products, brands, or categories" 285 aria-expanded="false" 286 aria-controls="SearchProductPreviews SearchSuggestions" 287 aria-describedby="SearchOverlayDescription" 288 /> 289 <span id="SearchOverlayDescription" class="sr-only">Type to search. Use arrow keys to navigate results. Press enter to view all results.</span> 290 <button 291 class="icon-Broswer-Close clear-search x" 292 id="ClearSearchInput" 293 style="display: none;" 294 aria-label="Clear search" 295 type="button" 296 ></button> 297 </div> 298 <button class="search-cancel mb0" id="SearchOverlayCancel">Cancel</button> 299 </div> 300 301 <div class="search-overlay-content"> 302 <!-- Left: Suggestions --> 303 <div class="search-suggestions"> 304 <!-- Popular Searches --> 305 <div class="suggestions-section" id="PopularSearches" role="region" aria-label="Popular searches"> 306 <h4 class="suggestions-title">Popular Searches</h4> 307 <ul id="PopularSearchesList" class="suggestions-list" role="list" aria-live="polite"> 308 <!-- Loading skeleton --> 309 <li class="suggestion-skeleton" aria-hidden="true"><span></span></li> 310 <li class="suggestion-skeleton" aria-hidden="true"><span></span></li> 311 <li class="suggestion-skeleton" aria-hidden="true"><span></span></li> 312 </ul> 313 </div> 314 315 <!-- Top Searched Brands --> 316 <div class="suggestions-section" id="TopBrands" role="region" aria-label="Top brands"> 317 <h4 class="suggestions-title">Top Brands</h4> 318 <ul id="TopBrandsList" class="suggestions-list" role="list" aria-live="polite"> 319 <!-- Loading skeleton --> 320 <li class="suggestion-skeleton"><span></span></li> 321 <li class="suggestion-skeleton"><span></span></li> 322 <li class="suggestion-skeleton"><span></span></li> 323 </ul> 324 </div> 325 326 <!-- Top Searched Categories --> 327 <div class="suggestions-section" id="TopCategories" role="region" aria-label="Top categories"> 328 <h4 class="suggestions-title">Top Categories</h4> 329 <ul id="TopCategoriesList" class="suggestions-list" role="list" aria-live="polite"> 330 <!-- Loading skeleton --> 331 <li class="suggestion-skeleton"><span></span></li> 332 <li class="suggestion-skeleton"><span></span></li> 333 <li class="suggestion-skeleton"><span></span></li> 334 </ul> 335 </div> 336 337 <!-- Dynamic Search Suggestions (shown when typing) --> 338 <div class="suggestions-section" id="SearchSuggestions" style="display: none;"> 339 <h4 class="suggestions-title">Top Suggestions</h4> 340 <ul id="SuggestionsList" class="suggestions-list"> 341 <!-- Dynamic suggestions populated here --> 342 </ul> 343 </div> 344 </div> 345 346 <!-- Right: Product Previews --> 347 <div class="search-products"> 348 <div class="search-loading-bar" id="SearchLoadingBar"> 349 <div class="search-loading-bar-progress"></div> 350 </div> 351 <div class="search-products-fade"></div> 352 <div id="SearchProductPreviews" class="product-previews-grid"> 353 <!-- Dynamic product tiles populated here --> 354 </div> 355 <div id="SearchSearching" class="search-searching" style="display: none;"> 356 <h3>Searching...</h3> 357 </div> 358 <div id="SearchEmptyState" class="search-empty-state" style="display: none;"> 359 <div class="icon-aisearch empty-icon"></div> 360 <p id="SearchEmptyStateText">Start Typing to Search</p> 361 <div class="recent-searches-inline mt8" id="RecentSearches" style="display: none;" role="region" aria-label="Recent searches"> 362 <div class="recent-searches-container">
363 <span class="recent-searches-label">Your Recent Searches:</span> 364 <span id="RecentSearchesList" class="recent-searches-links" role="list"> 365 <!-- Dynamic recent searches --> 366 </span> 367 </div> 368 </div> 369 </div> 370 <div id="SearchNoResults" class="search-no-results" style="display: none;"> 371 <div class="sad-search"></div> 372 <p>Hmmm... We didn't find any matches</p> 373 <span>Modify your search and try again</span> 374 </div> 375 </div> 376 </div> 377 378 <!-- Search All Button --> 379 <div class="search-overlay-footer"> 380 <button id="SearchAllButton" class="search-all-button"> 381 <div>Search All Products</div> 382 </button> 383 </div> 384</div> 385 386<!-- Product Preview Template --> 387<div id="SearchProductTemplate" class="search-product-tile product clickable template" data-url="{{url}}" style="display: none;"> 388 <div class="search-product-image image" style="background-image: url('{{product.url}}')"> 389 <span class="search-sale-badge{{onsale}}">SALE</span> 390 </div> 391 <div class="colorswatches" data-product-name="{{product.name}}" data-site-id="{{siteId}}" data-total-colors="{{totalColors}}"></div> 392 <div class="search-product-info"> 393 <div class="search-product-name">{{product.name}}</div> 394 <div class="search-product-brand">{{product.brandName}}</div> 395 <div class="search-product-price">{{product.price}}</div> 396 </div> 397</div> 398 399<!-- Color Swatches Component -->
400<script src="/assets/scripts/shared/colorSwatches.js"></script>
400 401 402 403 <div class="main-container"></div> 404 405 <!-- Search Overlay Script --> 406
406<script src="/assets/scripts/search-overlay.js?v=1.2.0"></script>
406 407
407<script> 408 // Initialize search overlay triggers - use immediate execution to avoid race conditions 409 (function() { 410 function attachSearchTriggers() { 411 var trigger = document.getElementById('SearchOverlayTrigger'); 412 413 if (trigger && typeof SearchOverlay !== 'undefined') { 414 // Remove any existing listener first 415 trigger.replaceWith(trigger.cloneNode(true)); 416 trigger = document.getElementById('SearchOverlayTrigger'); 417 418 trigger.addEventListener('click', function(e) { 419 e.preventDefault(); 420 e.stopPropagation(); 421 SearchOverlay.open(); 422 }); 423 } 424 } 425 426 // Attach immediately if DOM is ready, otherwise wait 427 if (document.readyState === 'loading') { 428 document.addEventListener('DOMContentLoaded', attachSearchTriggers); 429 } else { 430 attachSearchTriggers(); 431 } 432 })(); 433 434 // Sync mobile cart count with desktop cart count - run after DOM is ready 435 (function() { 436 function syncMobileCart() { 437 var desktopCount = document.getElementById('CartItemCount'); 438 var mobileCount = document.getElementById('MobileCartItemCount'); 439 if (desktopCount && mobileCount) { 440 mobileCount.textContent = desktopCount.textContent; 441 mobileCount.className = desktopCount.className; 442 } 443 } 444 445 function initCartSync() { 446 syncMobileCart(); 447 448 var observer = new MutationObserver(syncMobileCart); 449 var desktopCount = document.getElementById('CartItemCount'); 450 if (desktopCount) { 451 observer.observe(desktopCount, { childList: true, characterData: true, subtree: true }); 452 } 453 } 454 455 if (document.readyState === 'loading') { 456 document.addEventListener('DOMContentLoaded', initCartSync); 457 } else { 458 initCartSync(); 459 } 460 })(); 461 </script>
461<section class="page-title page-title-4 image-bg overlay hideshopheaders"> 462 <div class="background-image-holder"> 463 <img alt="Background Image testing" class="background-image" src="/branding/shop.runningcompany.com/running1.jpg" /> 464 </div> 465 <div class="headeroverlay"></div> 466 <div class="container wide"> 467 <div class="row"> 468 <div class="col-md-7"> 469 <h2 class="uppercase mb0 text-shadow">Shop Naperville Running</h2> 470 <p class="lead mb0 text-shadow"></p> 471 </div> 472 <div class="col-md-5 text-right"> 473 <ol class="breadcrumb breadcrumb-2 text-shadow"> 474 <li><a href="/">Shop</a></li> 475 <li id="CategoryName" class="active"></li> 476 </ol> 477 </div> 478 </div> 479 </div> 480</section> 481<div class="threshold hidden">Free Shipping<span class="freeterms hidden"> on orders totaling more than $<span id="Threshold" class="freeamount"></span></span></div> 482<section class="pt24 pt-xs-0 bggrey"> 483 <div class="container wide mb8"> 484 <div class="row"> 485 <div class="col-md-9 col-md-push-3"> 486 <div id="BrandingImage" class="col-md-12 mb16" style="display: none"> 487 <div class="row"> 488 <div class="page-title brandingimage image-bg"> 489 <img id="BrandImage" alt="Background Image" class="background-image" /> 490 </div> 491 </div> 492 </div> 493 494 <div class="row"> 495 <div class="col-sm-12 mb24 mb-sm-0"> 496 <div class="row search-page-container pl0 mb-sm-8 initializing"> 497 <div class="paging-container"> 498 <div class="paging-labels"> 499 <div id="PagingTotalsNone" class="col-md-9 col-sm-12 pageTotals"><span class="input-lh"> </span></div> 500 <div id="PagingTotals" class="col-md-9 col-sm-12 pageTotals"><span class="input-lh"><span id="SearchTotal"></span> Results</span></div> 501 </div> 502 <div id="MobileFilterButton" class="col-xs-6 visible-sm visible-xs pl0 pr1"> 503 <button id="OpenMobileFilters" class="btn" style="width: 100%; margin-bottom: 0;"> 504 Filters <span id="FilterBadge" class="filter-badge" style="display: none;"></span> <div class="filtericon"></div> 505 </button> 506 </div> 507 <div id="SortArea" class="col-md-3 col-sm-6 col-xs-6 page-select-container widget mb0 pl1 pr0"> 508 <div class="select-option sortby filter-content mb-sm-0 mb0"> 509 <i class="ti-angle-down"></i> 510 <select id="SortBy"> 511 <option selected value="brandName ASC">Sort By...</option> 512 <option value="relevance ASC">Relevance</option> 513 <option value="brandName ASC">Brand</option> 514 <option value="name ASC">Product Name</option> 515 <option value="cost DESC">Highest Price</option> 516 <option value="cost ASC">Lowest Price</option> 517 <option value="isFeatured DESC">Featured</option> 518 </select> 519 </div> 520 </div> 521 </div> 522 </div> 523 </div> 524 </div> 525 526 <div id="ProductList" class="grid responsive-product-grid initializing"> 527 528 <div id="InitialLoader" style="display: contents;"> 529 <div class="product skeleton-loader"> 530 <div class="thumb-loading"></div> 531 <div class="title-loading"></div> 532 <div class="brand-loading"></div> 533 </div> 534 <div class="product skeleton-loader"> 535 <div class="thumb-loading"></div> 536 <div class="title-loading"></div> 537 <div class="brand-loading"></div> 538 </div> 539 <div class="product skeleton-loader"> 540 <div class="thumb-loading"></div> 541 <div class="title-loading"></div> 542 <div class="brand-loading"></div> 543 </div> 544 <div class="product skeleton-loader"> 545 <div class="thumb-loading"></div> 546 <div class="title-loading"></div> 547 <div class="brand-loading"></div> 548 </div> 549 <div class="product skeleton-loader"> 550 <div class="thumb-loading"></div> 551 <div class="title-loading"></div> 552 <div class="brand-loading"></div> 553 </div> 554 <div class="product skeleton-loader"> 555 <div class="thumb-loading"></div> 556 <div class="title-loading"></div> 557 <div class="brand-loading"></div> 558 </div> 559 <div class="product skeleton-loader"> 560 <div class="thumb-loading"></div> 561 <div class="title-loading"></div> 562 <div class="brand-loading"></div> 563 </div> 564 <div class="product skeleton-loader"> 565 <div class="thumb-loading"></div> 566 <div class="title-loading"></div> 567 <div class="brand-loading"></div> 568 </div> 569 <div class="product skeleton-loader"> 570 <div class="thumb-loading"></div> 571 <div class="title-loading"></div> 572 <div class="brand-loading"></div> 573 </div> 574 <div class="product skeleton-loader"> 575 <div class="thumb-loading"></div> 576 <div class="title-loading"></div> 577 <div class="brand-loading"></div> 578 </div> 579 <div class="product skeleton-loader"> 580 <div class="thumb-loading"></div> 581 <div class="title-loading"></div> 582 <div class="brand-loading"></div> 583 </div> 584 <div class="product skeleton-loader"> 585 <div class="thumb-loading"></div> 586 <div class="title-loading"></div> 587 <div class="brand-loading"></div> 588 </div> 589 </div> 590 </div> 591 592 <!--end of row--> 593 <div class="paging col-sm-9 pl0 pr0"><div class="pages"></div></div> 594 <div id="PagingArea" class="pageCount col-sm-3 mt8 mb-xs-32"><span>Page <span id="SearchCurrentPage">1</span> of <span id="SearchTotalPages"></span></div> 595 596 </div> 597 <!--end of nine col--> 598 <div id="SearchFilterArea" class="col-md-3 col-md-pull-9 pt-xs-24"> 599 <div class="mobile-filter-group"> 600 <div id="PillBoxArea" class="widget mb-sm-16" style="display: none;"> 601 <ul id="PillBox" class="pillbox"></ul> 602 <div id="ClearFilterButton" class="clearfilters mt8 mt-sm-0" style="display: none;"><i class="fa fa-rotate-right red"></i> 603 Clear Filters</div> 604 </div> 605 <!--end of widget--> 606 <div id="FiltersArea"> 607 <div class="filter-container visible-sm visible-xs"> 608 <i id="FiltersExpand" class="filteropener toggleright"></i> 609 <div class="filtertoggletitle shopfilters mb0">Filter & Sort</div> 610 </div> 611 <div id="FiltersList"> 612 613 <div id="BrandArea" class="widget"> 614 <div class="filter-container"> 615 <i id="BrandsExpand" class="ti-angle-down toggleright"></i> 616 <div class="title shopfilters"> Brand</div> 617 </div> 618 <hr /> 619 <div class="filter-content"> 620 <input id="BrandSearchInput" class="mb16" type="text" placeholder="Search Brands..." /> 621 <div id="BrandCheckboxList" class="mt-sm-0 mb-sm-8"></div> 622 <div id="BrandShowMore" class="show-more-container mb-sm-16" style="display:none;"> 623 <a href="javascript:void(0);" class="show-more-link">Show <span id="BrandShowMoreCount"></span> more</a> 624 </div> 625 </div> 626 </div> 627 <!--end of widget--> 628 629 <div id="CategoryArea" class="widget"> 630 <div class="filter-container"> 631 <i id="CategoriesExpand" class="ti-angle-down toggleright"></i> 632 <div class="title shopfilters">Categories</div> 633 </div> 634 <hr /> 635 <div id="CategoryList" class="filter-content mt-sm-0 mb-sm-8" style="display:none"> 636 <input id="CategorySearchInput" class="mb16" type="text" placeholder="Search Categories..." /> 637 <div id="CategoryLinkList"></div> 638 <div id="CategoryShowMore" class="show-more-container mb-sm-16" style="display:none;"> 639 <a href="javascript:void(0);" class="show-more-link">Show <span id="CategoryShowMoreCount"></span> more</a> 640 </div> 641 </div> 642 </div> 643 644 <div id="CollectionArea" class="widget"> 645 <div class="filter-container"> 646 <i id="CollectionsExpand" class="ti-angle-down toggleright"></i> 647 <div class="title shopfilters">Collections</div> 648 </div> 649 <hr /> 650 <div class="filter-content"> 651 <input id="CollectionSearchInput" class="mb16" type="text" placeholder="Search Collections..." /> 652 <div id="CollectionCheckboxList" class="mt-sm-0 mb-sm-8"></div> 653 <div id="CollectionShowMore" class="show-more-container mb-sm-16" style="display:none;"> 654 <a href="javascript:void(0);" class="show-more-link">Show <span id="CollectionShowMoreCount"></span> more</a> 655 </div> 656 </div> 657 </div> 658 <!--end of widget--> 659 660 <div id="SizeArea" class="widget"> 661 <div class="filter-container"> 662 <i id="SizesExpand" class="ti-angle-down toggleright"></i> 663 <div class="title shopfilters">Sizes</div> 664 </div> 665 <hr /> 666 <div class="filter-content"> 667 <input id="SizeSearchInput" class="mb16" type="text" placeholder="Search Sizes..." /> 668 <div id="SizeCheckboxList" class="mt-sm-0 mb-sm-8"></div> 669 <div id="SizeShowMore" class="show-more-container mb-sm-16" style="display:none;"> 670 <a href="javascript:void(0);" class="show-more-link">Show <span id="SizeShowMoreCount"></span> more</a> 671 </div> 672 </div> 673 </div> 674 675 <div id="WidthArea" class="widget bridaloff"> 676 <div class="filter-container"> 677 <i id="WidthsExpand" class="ti-angle-down toggleright"></i> 678 <div class="title shopfilters">Styles</div> 679 </div> 680 <hr /> 681 <div class="filter-content"> 682 <input id="WidthSearchInput" class="mb16" type="text" placeholder="Search Styles..." /> 683 <div id="WidthCheckboxList" class="mt-sm-0 mb-sm-8"></div> 684 <div id="WidthShowMore" class="show-more-container mb-sm-16" style="display:none;"> 685 <a href="javascript:void(0);" class="show-more-link">Show <span id="WidthShowMoreCount"></span> more</a> 686 </div> 687 </div> 688 </div> 689 690 <div id="PriceArea" class="widget"> 691 <div class="filter-container"> 692 <i id="PriceExpand" class="ti-angle-down toggleright"></i> 693 <div class="title shopfilters">Price</div> 694 </div> 695 <hr /> 696 <div class="filter-content"> 697 <div id="PriceCheckboxList" class="mt-sm-0 mb-sm-24"></div> 698 <div class="mt-sm-0 mb-sm-16"> 699 <div class="price-filter-grid"> 700 <input type="text" id="PriceMin" data-filter="price-min" placeholder="Min" class="mb0" />
701 <span class="price-dash">-</span> 702 <input type="text" id="PriceMax" data-filter="price-max" placeholder="Max" class="mb0" /> 703 <button id="ApplyCustomPrice" data-filter="apply-price" class="btn btn-sm mb0">Apply</button> 704 </div> 705 </div> 706 </div> 707 </div> 708 709 <div id="GenderArea" class="widget"> 710 <div class="filter-container"> 711 <i id="GenderExpand" class="ti-angle-down toggleright"></i> 712 <div class="title shopfilters">Gender</div> 713 </div> 714 <hr /> 715 <div class="filter-content"> 716 <div id="GenderCheckboxList" class="mt-sm-0 mb-sm-24"> 717 <div class="input-checkbox"> 718 <input type="checkbox" id="GenderMens" name="gender-filter" value="M" /> 719 <label for="GenderMens" class="runfreecheck"></label> 720 <span class="checkboxlabel">Mens</span> 721 </div> 722 <div class="input-checkbox"> 723 <input type="checkbox" id="GenderWomens" name="gender-filter" value="W" /> 724 <label for="GenderWomens" class="runfreecheck"></label> 725 <span class="checkboxlabel">Womens</span> 726 </div> 727 <div class="input-checkbox"> 728 <input type="checkbox" id="GenderUnisex" name="gender-filter" value="U" /> 729 <label for="GenderUnisex" class="runfreecheck"></label> 730 <span class="checkboxlabel">Unisex</span> 731 </div> 732 <div class="input-checkbox"> 733 <input type="checkbox" id="GenderKids" name="gender-filter" value="K" /> 734 <label for="GenderKids" class="runfreecheck"></label> 735 <span class="checkboxlabel">Kids</span> 736 </div> 737 </div> 738 </div> 739 </div> 740 741 <div id="ColorArea" class="widget"> 742 <div class="filter-container"> 743 <i id="ColorExpand" class="ti-angle-down toggleright"></i> 744 <div class="title shopfilters">Color</div> 745 </div> 746 <hr /> 747 <div class="filter-content"> 748 <div id="ColorCheckboxList" class="mt-sm-0 mb-sm-8"></div> 749 <div id="ColorShowMore" class="show-more-container mb-sm-16" style="display:none;"> 750 <a href="javascript:void(0);" class="show-more-link">Show <span id="ColorShowMoreCount"></span> more</a> 751 </div> 752 </div> 753 </div> 754 <!--end of widget--> 755 756 <div id="ShippingArea" class="widget" style="display:none"> 757 <div class="filter-container"> 758 <i id="ShippingExpand" class="ti-angle-down toggleright"></i> 759 <div class="title shopfilters">Options</div> 760 </div> 761 <hr /> 762 <div id="ShippingList" class="filter-content mt-sm-0 mb-sm-8"> 763 <div class="input-checkbox" style="display:none;" > 764 <input id="ShipToMe" data-filter="ship-to-me" type="checkbox" name="agree" /> 765 <label for="ShipToMe" class="runfreecheck"></label> 766 <span class="checkboxlabel">Ship To Me</span> 767 </div> 768 <div style="display:none;" class="input-checkbox" > 769 <input id="PickupInStore" data-filter="pickup-in-store" type="checkbox" name="agree" /> 770 <label for="PickupInStore" class="runfreecheck"></label> 771 <span class="checkboxlabel">Pickup In-Store</span> 772 </div> 773 <div class="input-checkbox" > 774 <input id="OnSale" data-filter="on-sale" type="checkbox" name="agree" /> 775 <label for="OnSale" class="runfreecheck"></label> 776 <span class="checkboxlabel">Sale Products</span> 777 </div> 778 </div> 779 </div> 780 </div> 781 </div> 782 </div> 783 <!--end of widget--> 784 <div id="ReturnPolicy" class="widget mb8"> 785 <div class="filter-container"> 786 <i id="ReturnExpand" class="ti-angle-down toggleright"></i> 787 <div class="title">Return Policy</div> 788 </div> 789 <hr/> 790 <p id="miniReturn">We want you to be 100% satisfied with your purchase. If you're not happy with the product, please contact us before 30 days and we'll make sure you're taken care of. </p> 791 </div> 792 793 <!--end of widget--> 794 <div id="PopularArea" class="widget" style="display:none"> 795 <div class="filter-container"> 796 <i id="PopularExpand" class="ti-angle-down toggleright"></i> 797 <div class="title">Featured Items</div> 798 </div> 799 <hr /> 800 <div id="PopularList" class="cart-overview"></div> 801 </div> 802 <!--end of widget--> 803 </div> 804 <!--end of sidebar--> 805 </div> 806 <!--end of container row--> 807 </div> 808 <!--end of container--> 809</section> 810 811<!-- Mobile Filter Panel --> 812<div id="MobileFilterOverlay" class="mobile-filter-overlay"></div> 813<div id="MobileFilterPanel" class="mobile-filter-panel"> 814 <div class="mobile-filter-header"> 815 <h3 class="mobile-filter-title">Filters</h3> 816 <button id="CloseMobileFilters" class="mobile-filter-close"> 817 <i class="icon-x"></i> 818 </button> 819 </div> 820 <div id="MobilePillBoxArea" class="mobile-pill-box-area" style="display: none;"> 821 <ul id="MobilePillBox" class="pillbox"></ul> 822 <div id="MobileClearFilterButton" class="clearfilters mt8 mt-sm-0" style="display: none;"><i class="fa fa-rotate-right red"></i> 823 Clear Filters</div> 824 </div>
825 <div id="MobileFilterContent" class="mobile-filter-content"> 826 <!-- Filters will be cloned here --> 827 </div> 828 <div class="mobile-filter-footer"> 829 <button id="ApplyMobileFilters" class="btn btn-primary mb0 ml0"> 830 View <span id="MobileFilterCount"></span> Products 831 </button> 832 </div> 833</div> 834 835<!-- Templates --> 836<div id="CardLoader" style="display: none;"> 837 <div class="cardloader" id="loading"> 838 <div class="thumb-loading"></div> 839 <div class="title-loading"></div> 840 <div class="brand-loading"></div> 841 <div class="loadtext">Loading...</div> 842 </div> 843 <div class="cardloader" id="loading"> 844 <div class="thumb-loading"></div> 845 <div class="title-loading"></div> 846 <div class="brand-loading"></div> 847 <div class="loadtext">Loading...</div> 848 </div> 849 <div class="cardloader" id="loading"> 850 <div class="thumb-loading"></div> 851 <div class="title-loading"></div> 852 <div class="brand-loading"></div> 853 <div class="loadtext">Loading...</div> 854 </div> 855 <div class="cardloader" id="loading"> 856 <div class="thumb-loading"></div> 857 <div class="title-loading"></div> 858 <div class="brand-loading"></div> 859 <div class="loadtext">Loading...</div> 860 </div> 861 <div class="cardloader" id="loading"> 862 <div class="thumb-loading"></div> 863 <div class="title-loading"></div> 864 <div class="brand-loading"></div> 865 <div class="loadtext">Loading...</div> 866 </div> 867 <div class="cardloader" id="loading"> 868 <div class="thumb-loading"></div> 869 <div class="title-loading"></div> 870 <div class="brand-loading"></div> 871 <div class="loadtext">Loading...</div> 872 </div> 873 <div class="cardloader" id="loading"> 874 <div class="thumb-loading"></div> 875 <div class="title-loading"></div> 876 <div class="brand-loading"></div> 877 <div class="loadtext">Loading...</div> 878 </div> 879 <div class="cardloader" id="loading"> 880 <div class="thumb-loading"></div> 881 <div class="title-loading"></div> 882 <div class="brand-loading"></div> 883 <div class="loadtext">Loading...</div> 884 </div> 885 <div class="cardloader" id="loading"> 886 <div class="thumb-loading"></div> 887 <div class="title-loading"></div> 888 <div class="brand-loading"></div> 889 <div class="loadtext">Loading...</div> 890 </div> 891 <div class="cardloader" id="loading"> 892 <div class="thumb-loading"></div> 893 <div class="title-loading"></div> 894 <div class="brand-loading"></div> 895 <div class="loadtext">Loading...</div> 896 </div> 897 <div class="cardloader" id="loading"> 898 <div class="thumb-loading"></div> 899 <div class="title-loading"></div> 900 <div class="brand-loading"></div> 901 <div class="loadtext">Loading...</div> 902 </div> 903 <div class="cardloader" id="loading"> 904 <div class="thumb-loading"></div> 905 <div class="title-loading"></div> 906 <div class="brand-loading"></div> 907 <div class="loadtext">Loading...</div> 908 </div> 909</div> 910 911<style> 912 @keyframes fadeInUp { 913 from { 914 opacity: 0; 915 transform: translateY(60px); 916 } 917 to { 918 opacity: 1; 919 transform: translateY(0); 920 } 921} 922#SearchFilterArea { 923 opacity: 1; 924 /** animation: fadeInUp 0.9s cubic-bezier(0.22, 0.61, 0.36, 1) forwards; **/ 925} 926 927/* Generic Filter Styles */ 928#BrandCheckboxList, 929#SizeCheckboxList, 930#WidthCheckboxList, 931#PriceCheckboxList, 932#GenderCheckboxList { 933 margin-top: 0; 934 margin-bottom: 8px; 935} 936 937/* Hide native checkbox inputs - custom styling via .runfreecheck label */ 938.input-checkbox input[type="checkbox"] { 939 position: absolute; 940 opacity: 0; 941 pointer-events: none; 942} 943 944/* Add left margin to align checkboxes with section headers */ 945.input-checkbox { 946 margin-left: 17px; 947} 948 949.show-more-container { 950 margin-top: 8px; 951} 952 953.show-more-link, 954.show-more-link span { 955 color: var(--almostblack )!important; 956 text-decoration: underline !important; 957 font-size: 15px !important; 958 font-weight: 600 !important; 959} 960 961.show-more-link:hover, 962.show-more-link:hover span { 963 color: var(--primary)!important; 964 filter: unset; 965} 966 967.show-more-link:active { 968 filter: brightness(75%); 969 transform: translateY(1px)!important; 970 transition: 0.3s;
971} 972 973/* Price filter grid */ 974.price-filter-grid { 975 display: grid; 976 gap: 8px; 977 align-items: center; 978 margin-bottom: 8px; 979 grid-template-columns: 70px 5px 70px 100px; 980} 981 982.price-dash { 983 color: var(--almostblack); 984 margin-bottom: 4px; 985 text-align: center; 986} 987 988/* Wrap Apply button to new line on tablet and below when sidebar is narrower */ 989@media (max-width: 1200px) { 990 .price-filter-grid { 991 grid-template-columns: 70px 5px 70px; 992 } 993 994 .price-filter-grid #ApplyCustomPrice { 995 grid-column: 1 / -1; 996 width: 100%; 997 } 998} 999 1000/* Color filter circles */ 1001.color-circle { 1002 width: 24px; 1003 height: 24px; 1004 border-radius: 50%; 1005 display: inline-block; 1006 margin-right: 8px; 1007 border: 2px solid #e0e0e0; 1008 vertical-align: middle; 1009 position: relative; 1010 top: -1px; 1011} 1012 1013.input-checkbox .color-circle { 1014 margin-left: 8px; 1015} 1016 1017/* Make filter containers clickable */ 1018.filter-container { 1019 cursor: pointer; 1020 user-select: none; 1021} 1022 1023.filter-container:hover .ti-angle-down.toggleright { 1024 color: var(--almostblack); 1025} 1026 1027/* Category Filter Styles */ 1028.category-item { 1029 display: block; 1030 margin-bottom: 8px; 1031} 1032 1033.category-toggle { 1034 cursor: pointer; 1035 font-size: 12px; 1036 margin-right: 10px; 1037 vertical-align: middle; 1038 color: var(--filterhr); 1039 font-weight: 100; 1040 transition: 0.3s; 1041} 1042 1043.category-toggle:hover { 1044 color: var(--almostblack); 1045 transition: 0.3s; 1046} 1047 1048.category-toggle:active { 1049 filter: brightness(75%); 1050 transform: translateY(1px); 1051 transition: 0.3s; 1052} 1053 1054.category-link { 1055 color: var(--menu); 1056 text-decoration: none; 1057} 1058 1059.category-link.selected { 1060 font-weight: 700; 1061 color: var(--primary)!important; 1062 transition: 0.3s; 1063} 1064 1065.category-link:hover { 1066 color: var(--primary); 1067 text-decoration: underline; 1068 filter: unset; 1069 transition: 0.3s; 1070} 1071 1072.category-link:active { 1073 filter: brightness(80%); 1074 transition: 0.3s; 1075} 1076 1077.category-spacer { 1078 display: inline-block; 1079 width: 20px; 1080} 1081 1082.category-children { 1083 display: none; 1084 margin-left: 30px; 1085 margin-top: 5px; 1086} 1087 1088.category-child-item { 1089 margin-bottom: 5px; 1090} 1091 1092/* Prevent body scroll when mobile filter is open - iOS fix */ 1093body.mobile-filter-open { 1094 overflow: hidden !important; 1095 position: fixed; 1096 width: 100%; 1097 height: 100%; 1098 touch-action: none; 1099 -webkit-overflow-scrolling: none; 1100} 1101 1102/* Mobile Filter Panel Styles */ 1103.mobile-filter-overlay { 1104 display: none; 1105 position: fixed; 1106 top: 0; 1107 left: 0; 1108 right: 0; 1109 bottom: 0; 1110 width: 100%; 1111 height: 100%; 1112 background: var(--bggrey); 1113 z-index: 9998; 1114 opacity: 0; 1115 transition: opacity 0.3s ease; 1116} 1117 1118.mobile-filter-overlay.active { 1119 display: block; 1120 opacity: 1; 1121} 1122 1123.mobile-filter-panel { 1124 position: fixed; 1125 top: 0; 1126 left: 0; 1127 right: 0; 1128 bottom: 0; 1129 width: 100%; 1130 background: var(--white); 1131 z-index: 9999; 1132 display: flex; 1133 flex-direction: column; 1134 transform: translateX(-100%); 1135 transform-origin: left center; 1136 will-change: transform; 1137 overflow: hidden; 1138} 1139 1140.mobile-filter-panel.active { 1141 animation: slideInFromLeft 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; 1142} 1143 1144.count { 1145 display: none !important; 1146} 1147 1148@keyframes slideInFromLeft { 1149 0% { 1150 transform: translateX(-100%) scaleX(0.95); 1151 } 1152 50% { 1153 transform: translateX(0) scaleX(1.03); 1154 } 1155 65% { 1156 transform: translateX(0) scaleX(0.98); 1157 } 1158 80% { 1159 transform: translateX(0) scaleX(1.01); 1160 } 1161 100% { 1162 transform: translateX(0) scaleX(1); 1163 } 1164} 1165 1166.mobile-filter-header { 1167 position: sticky; 1168 top: 0; 1169 background: var(--bggrey); 1170 padding: 15px 20px 15px 20px; 1171 border-bottom: 1px solid var(--border); 1172 display: flex; 1173 justify-content: space-between; 1174 align-items: center; 1175 z-index: 10; 1176} 1177 1178.mobile-pill-box-area { 1179 position: sticky; 1180 top: 69px; 1181 background: white; 1182 padding: 12px 20px; 1183 border-bottom: 1px solid var(--border); 1184 z-index: 9; 1185} 1186 1187.mobile-pill-box-area .pillbox { 1188 margin-bottom: 0; 1189} 1190 1191.mobile-pill-box-area .clearfilters { 1192 margin-top: 8px; 1193} 1194 1195.mobile-filter-title { 1196 margin: 0; 1197 font-size: 20px; 1198 font-weight: 700;
1199 color: var(--almostblack); 1200} 1201 1202.mobile-filter-close { 1203 background: none; 1204 border: none; 1205 font-size: 25px; 1206 color: var(--almostblack); 1207 cursor: pointer; 1208 padding: 0; 1209 width: 32px; 1210 height: 32px; 1211 font-weight: 800; 1212 display: flex; 1213 align-items: center; 1214 justify-content: center; 1215 margin-bottom: 0px; 1216} 1217 1218.mobile-filter-close:hover { 1219 color: var(--primary); 1220} 1221 1222.mobile-filter-content { 1223 flex: 1; 1224 overflow-y: auto; 1225 overflow-x: hidden; 1226 padding: 20px; 1227 -webkit-overflow-scrolling: touch; 1228} 1229 1230.mobile-filter-footer { 1231 position: sticky; 1232 bottom: 0; 1233 background: var(--bggrey); 1234 padding: 16px 20px; 1235 border-top: 1px solid var(--border); 1236 z-index: 10; 1237 text-align: center; 1238} 1239 1240#ApplyMobileFilters { 1241 width: 225px; 1242 height: auto; 1243 margin: 0 auto; 1244 position: relative; 1245 cursor: pointer; 1246 transition: all 0.2s ease; 1247 display: inline; 1248 align-items: center; 1249 justify-content: center; 1250 font-family: inherit; 1251 margin-bottom: 0px; 1252 border: 2px solid transparent; 1253 border-radius: 12px; 1254 padding: 3px 20px; 1255 font-size: 16px; 1256 font-weight: 700; 1257 color: #ffffff; 1258 overflow: visible; 1259 -webkit-tap-highlight-color: transparent; 1260 touch-action: manipulation; 1261 /* Rainbow gradient background with multiple layers */ 1262 background: 1263 linear-gradient(#121213, #121213) padding-box, 1264 linear-gradient(#121213 50%, rgba(18, 18, 19, 0.6) 80%, rgba(18, 18, 19, 0)) border-box, 1265 linear-gradient(90deg, 1266 #0fb865, 1267 #3285ff, 1268 #f94543, 1269 #fabd10 1270 ) border-box; 1271 background-size: 200%; 1272 background-clip: padding-box, border-box, border-box; 1273 background-origin: border-box; 1274 animation: rainbow 2s linear infinite; 1275} 1276 1277#ApplyMobileFilters::before { 1278 content: ''; 1279 position: absolute; 1280 bottom: -30%; 1281 left: 50%; 1282 z-index: 0; 1283 height: 30%; 1284 width: 90%; 1285 transform: translateX(-50%); 1286 background: linear-gradient(90deg, 1287 #0fb865, 1288 #3285ff, 1289 #f94543, 1290 #fabd10 1291 ); 1292 filter: blur(20px); 1293 opacity: 0.8; 1294 animation: rainbow 2s linear infinite; 1295} 1296 1297#ApplyMobileFilters span, 1298#ApplyMobileFilters i, 1299#ApplyMobileFilters div { 1300 position: relative; 1301 z-index: 1; 1302} 1303 1304#ApplyMobileFilters:hover { 1305 transform: scale(1.02); 1306} 1307 1308#ApplyMobileFilters:active { 1309 transform: translateY(2px); 1310} 1311 1312#MobileFilterCount { 1313 font-weight: 700; 1314} 1315 1316/** 1317#ApplyMobileFilters { 1318 color: #fff; 1319 background-color: var(--primary); 1320 border-color: var(--primary); 1321} 1322 1323#ApplyMobileFilters:focus { 1324 color: #fff; 1325 background-color: var(--primary); 1326 border-color: var(--primary); 1327} 1328 1329#ApplyMobileFilters span { 1330 font-weight: 800; 1331} 1332 **/ 1333 1334#OpenMobileFilters { 1335 background-color: var(--white); 1336 border-color: var(--white); 1337 height: 50px; 1338 border-radius: 0px; 1339 color: var(--almostblack); 1340 margin-left:0px; 1341 outline: white; 1342 font-size: 14px; 1343 font-weight: 700; 1344 text-align: left; 1345 animation: fadeIn 1.75s ease forwards; 1346} 1347 1348#OpenMobileFilters:hover { 1349 filter: brightness(90%); 1350} 1351 1352#OpenMobileFilters:active { 1353 box-shadow: unset; 1354} 1355 1356#OpenMobileFilters span { 1357 font-size: 14px; 1358} 1359 1360.filter-badge { 1361 display: inline-block; 1362 min-width: 18px; 1363 height: 18px; 1364 padding: 3px 3px; 1365 font-size: 11px !important; 1366 font-weight: 700; 1367 line-height: 12px; 1368 color: var(--white); 1369 text-align: center; 1370 white-space: nowrap; 1371 vertical-align: middle; 1372 background-color: var(--almostblack); 1373 border-radius: 18px; 1374 margin-left: 4px; 1375 margin-top: -1px; 1376} 1377 1378/* Button Loading Spinner Styles */ 1379#ApplyMobileFilters.loading { 1380 position: relative !important; 1381 pointer-events: none; 1382} 1383 1384#ApplyMobileFilters.loading::before { 1385 content: ""; 1386 position: absolute; 1387 right: 18px; 1388 top: 50%; 1389 transform: translateY(-50%); 1390 width: 16px; 1391 height: 16px; 1392 border: 2px solid rgba(255, 255, 255, 0.3); 1393 border-top-color: white; 1394 border-radius: 50%; 1395 animation: buttonSpinner 0.8s linear infinite; 1396} 1397 1398@keyframes buttonSpinner { 1399 to { 1400 transform: translateY(-50%) rotate(360deg); 1401 } 1402} 1403 1404.filtericon {
1405 width: 20px; 1406 height: 20px; 1407 margin-bottom: 0px; 1408 margin-top: 8px; 1409 margin-right: -7px; 1410 background-image: url(/assets/images/filters.svg); 1411 background-size: 20px; 1412 background-repeat: no-repeat; 1413 float: right; 1414} 1415 1416 1417/* Hide desktop filter area on mobile when panel is used */ 1418@media (max-width: 975px) { 1419 .mobile-filter-group { 1420 display: none !important; 1421 } 1422 1423 /* Ensure Sort By stays visible on mobile */ 1424 #SortArea { 1425 display: block !important; 1426 } 1427 1428 /* Mobile filter/sort button row */ 1429 #MobileFilterButton, 1430 #SortArea { 1431 margin-bottom: 0px; 1432 } 1433 1434 .pr1 { 1435 padding-right: 1px; 1436 } 1437 1438 .pl1 { 1439 padding-left: 1px; 1440 } 1441 1442 1443 /* Hide paging labels initially to prevent layout shift */ 1444 .paging-labels { 1445 position: absolute; 1446 visibility: hidden; 1447 } 1448 1449 /* Show paging labels after they've been repositioned */ 1450 .paging-labels.positioned { 1451 position: static; 1452 visibility: visible; 1453 } 1454} 1455 1456/* Hide SSR products until initialization is complete */ 1457#ProductList.initializing .product:not(.skeleton-loader) { 1458 display: none !important; 1459} 1460/* Style skeleton loaders */ 1461.skeleton-loader { 1462 pointer-events: none; 1463 background: var(--white); 1464} 1465/* Reserve space for paging/sort to prevent layout jump - desktop only */ 1466@media (min-width: 768px) { 1467 .search-page-container { 1468 min-height: 48px; 1469 } 1470 /* Collapse during initialization to match skeleton position */ 1471 .search-page-container.initializing { 1472 min-height: 0 !important; 1473 height: 0; 1474 overflow: hidden; 1475 } 1476} 1477/* Also collapse the parent container's margin during initialization */ 1478.col-sm-12:has(.search-page-container.initializing) { 1479 margin-bottom: 64px !important; 1480} 1481/* Hide paging/sort areas during initialization to prevent layout jump */ 1482.search-page-container.initializing .paging-labels, 1483.search-page-container.initializing #SortArea { 1484 opacity: 0; 1485 visibility: hidden; 1486} 1487/* Fade in paging/sort areas after initialization */ 1488.search-page-container:not(.initializing) .paging-labels, 1489.search-page-container:not(.initializing) #SortArea { 1490 animation: fadeIn 1s ease forwards; 1491} 1492@keyframes fadeIn { 1493 from { 1494 opacity: 0; 1495 } 1496 to { 1497 opacity: 1; 1498 } 1499} 1500</style> 1501 1502<!-- Wishlist Login Modal --> 1503<div id="WishlistLoginModal" class="modal box"> 1504 <div class="close"></div> 1505 <div class="modal-header"> 1506 <div class="wishlist-color-modal mb24"></div> 1507 <div> 1508 <div class="title mt0 text-left">Save to Wishlist</div> 1509 <div class="subtitle mt0 mb8">Please login or sign up to save items to your wishlist</div> 1510 </div> 1511 </div> 1512 <div class="grid two" style="gap: 12px; margin-top: 24px;"> 1513 <a href="/login" class="button hollow" style="text-decoration: none; color: white; text-align: center;" onclick="modal.close();">Login</a> 1514 <a href="/signup" class="button" style="text-decoration: none; color: white; text-align: center;" onclick="modal.close();">Sign Up</a> 1515 </div> 1516</div> 1517 1518<!-- Repeater Templates --> 1519<div id="RowTemplate" class="product template clickable" style="display:none"> 1520 <span class="salelabel"><span class="salelabel-text">SALE</span></span> 1521 <span class="releaselabel"></span> 1522 <button class="wishlist-toggle inactive" data-product-id="" aria-label="Add to wishlist"> 1523 <div class="btn__effect"> 1524 <svg class="heart-stroke" viewBox="20 18 29 28" aria-hidden="true" focusable="false"><path d="M28.3 21.1a4.3 4.3 0 0 1 4.1 2.6 2.5 2.5 0 0 0 2.3 1.7c1 0 1.7-.6 2.2-1.7a3.7 3.7 0 0 1 3.7-2.6c2.7 0 5.2 2.7 5.3 5.8.2 4-5.4 11.2-9.3 15a2.8 2.8 0 0 1-2 1 3.4 3.4 0 0 1-2.2-1c-9.6-10-9.4-13.2-9.3-15 0-1 .6-5.8 5.2-5.8m0-3c-5.3 0-7.9 4.3-8.2 8.5-.2 3.2.4 7.2 10.2 17.4a6.3 6.3 0 0 0 4.3 1.9 5.7 5.7 0 0 0 4.1-1.9c1.1-1 10.6-10.7 10.3-17.3-.2-4.6-4-8.6-8.4-8.6a7.6 7.6 0 0 0-6 2.7 8.1 8.1 0 0 0-6.2-2.7z"></path></svg> 1525 <svg class="heart-full" viewBox="0 0 19.2 18.5" aria-hidden="true" focusable="false"><path d="M9.66 18.48a4.23 4.23 0 0 1-2.89-1.22C.29 10.44-.12 7.79.02 5.67.21 2.87 1.95.03 5.42.01c1.61-.07 3.16.57 4.25 1.76A5.07 5.07 0 0 1 13.6 0c2.88 0 5.43 2.66 5.59 5.74.2 4.37-6.09 10.79-6.8 11.5-.71.77-1.7 1.21-2.74 1.23z"></path></svg> 1526 <svg class="broken-heart" xmlns="http://www.w3.org/2000/svg" width="32" height="12" viewBox="5.707 17 32 12"><g fill="var(--red)"><path class="broken-heart--left" d="M21.865 28.735V18.703a3.562 3.562 0 0 0-2.567-1.476c-2.916.017-3.378 2.403-3.538 4.756-.118 1.781.227 4.006 4.672 7.737a2.544 2.544 0 0 0 1.428 1.025l-.008-.008.013-.002z"/><path class="broken-heart--right" d="M27.868 22.045c-.135-2.588-1.277-4.823-3.697-4.823a3.258 3.258 0 0 0-2.302 1.487l-.004-.003v10.035a2.215 2.215 0 0 0 1.289-1.033c.598-.596 4.882-5.99 4.714-9.663z"/></g><path class="broken-heart--crack" fill="none" stroke="#FFF" stroke-miterlimit="10" d="M21.865 18.205v10.573"/></svg> 1527 <div class="effect-group">
1528 <span class="effect"></span> 1529 <span class="effect"></span> 1530 <span class="effect"></span> 1531 <span class="effect"></span> 1532 <span class="effect"></span> 1533 </div> 1534 </div> 1535 </button> 1536 <div class="image"></div> 1537 <div class="colorswatches" data-product-name="" data-site-id="" data-total-colors=""></div> 1538 <div class="name"></div> 1539 <div class="brand uppercase"></div> 1540 <div class="price"></div> 1541 <a href="" rel="cononical" aria-label=""></a> 1542</div> 1543 1544<div id="PopularTemplate" class="template popular grid split clicker featured" style="display:none"> 1545 <div class="image mini" style="background-image: url('{{product.url}}')"></div> 1546 <div class="description"> 1547 <span class="name">{{product.name}}</span> 1548 <span class="price">${{product.price}}</span> 1549 <a href="{{url}}"></a> 1550 </div> 1551</div> 1552
1553<script> 1554// Apply stagger animation to products dynamically 1555document.addEventListener('DOMContentLoaded', function() { 1556 // Apply stagger animation to SSR products 1557 document.querySelectorAll('.product').forEach((el, i) => el.style.setProperty('--i', i + 1)); 1558 1559 // Check if we have filters in the URL that will cause JS filtering 1560 const urlParams = new URLSearchParams(window.location.search); 1561 const hasFilters = urlParams.has('category') || urlParams.has('brand') || 1562 urlParams.has('size') || urlParams.has('width') || 1563 urlParams.has('search') || urlParams.has('q') || 1564 urlParams.has('c') || urlParams.has('sale'); 1565 1566 // If no filters, reveal products immediately 1567 // If filters exist, keep initializing state until JS filtering completes 1568 if (!hasFilters) { 1569 $('#ProductList').removeClass('initializing'); 1570 $('#InitialLoader').remove(); 1571 $('.search-page-container').removeClass('initializing'); 1572 } 1573 1574 // Attach click handlers 1575 $('.clickable').off('click').on('click', page.goto); 1576 1577 // Load color swatches and wishlist asynchronously after products are visible 1578 // This prevents blocking the animation 1579 setTimeout(function() { 1580 // Initialize color swatches 1581 colorSwatches.init('#ProductList'); 1582 colorSwatches.load(); 1583 1584 // Re-render swatches after layout is complete to get accurate container widths 1585 setTimeout(function() { 1586 colorSwatches.load(true); // Force refresh with correct widths 1587 }, 500); 1588 1589 // Attach wishlist toggle handlers directly to SSR products 1590 // Must be attached before .clickable handlers fire 1591 // Attach wishlist toggle handlers 1592 $('.wishlist-toggle').off('click').on('click', page.toggleWishlist); 1593 1594 // Update wishlist button states asynchronously 1595 if (page && page.updateWishlistButtons) { 1596 if (wishlist && wishlist.isLoaded) { 1597 page.updateWishlistButtons(); 1598 } else { 1599 // Wait for wishlist to load in background 1600 var checkWishlist = setInterval(function() { 1601 if (wishlist && wishlist.isLoaded) { 1602 clearInterval(checkWishlist); 1603 page.updateWishlistButtons(); 1604 } 1605 }, 50); 1606 // Stop checking after 5 seconds 1607 setTimeout(function() { 1608 clearInterval(checkWishlist); 1609 }, 5000); 1610 } 1611 } 1612 }, 0); 1613}); 1614</script>
1614 1615
1616<script src="/assets/scripts/shared/colorSwatches.js"></script>
vendor: 2 bytes, line 1616
1616
1617<script src="/assets/scripts/productRelease.js"></script>
vendor: 2 bytes, line 1617
1617
1618<script src="/assets/scripts/collection.js"></script>
1618 1619
1620<script language="javascript" type="text/javascript"> 1621 var threshold = Number('100'); 1622 var isAutoSale = 'false'=='true'; 1623 var flatRate = Number('8'); 1624 var shipType = 'flat'; 1625 var inStoreAllowed = 'true'; 1626 var defaultSort = 'isFeatured DESC'; 1627 var brandImage = ''; 1628 var proSizes = '[{"size":"0","productCount":24},{"size":"1","productCount":25},{"size":"1.5","productCount":28},{"size":"2","productCount":18},{"size":"2.5","productCount":27},{"size":"3","productCount":18},{"size":"3.5","productCount":45},{"size":"4","productCount":44},{"size":"4.5","productCount":138},{"size":"5","productCount":243},{"size":"5.5","productCount":827},{"size":"6","productCount":306},{"size":"6.5","productCount":996},{"size":"7","productCount":514},{"size":"7.5","productCount":1769},{"size":"8","productCount":1373},{"size":"8.5","productCount":1875},{"size":"9","productCount":1414},{"size":"9.5","productCount":1976},{"size":"10","productCount":1426},{"size":"10.5","productCount":1889},{"size":"11","productCount":568},{"size":"11.5","productCount":1386},{"size":"12","productCount":456},{"size":"12.5","productCount":845},{"size":"13","productCount":709},{"size":"13.5","productCount":26},{"size":"14","productCount":271},{"size":"15","productCount":540},{"size":"16","productCount":35},{"size":"17","productCount":4},{"size":"18","productCount":3},{"size":"01","productCount":1},{"size":"02","productCount":2},{"size":"02.5","productCount":1},{"size":"03","productCount":1},{"size":"03.5","productCount":1},{"size":"04","productCount":7},{"size":"04.5","productCount":2},{"size":"05","productCount":13},{"size":"05.5","productCount":6},{"size":"06","productCount":34},{"size":"06.5","productCount":72},{"size":"07","productCount":135},{"size":"07.5","productCount":139},{"size":"08","productCount":177},{"size":"08.5","productCount":229},{"size":"09","productCount":273},{"size":"09.5","productCount":267},{"size":"36","productCount":6},{"size":"37","productCount":9},{"size":"38","productCount":11},{"size":"39","productCount":12},{"size":"40","productCount":9},{"size":"41","productCount":16},{"size":"42","productCount":6},{"size":"43","productCount":5},{"size":"44","productCount":6},{"size":"45","productCount":6},{"size":"46","productCount":5}]'; 1629 var proWidths = '[{"sizeAppend":"2A","width":"2A","productCount":26},{"sizeAppend":"2E","width":"2E","productCount":123},{"sizeAppend":"4E","width":"4E","productCount":49},{"sizeAppend":"6E","width":"6E","productCount":6},{"sizeAppend":"B","width":"B","productCount":526},{"sizeAppend":"D","width":"D","productCount":763},{"sizeAppend":"EE","width":"EE","productCount":183},{"sizeAppend":"EEEE","width":"EEEE","productCount":22},{"sizeAppend":"L","width":"L","productCount":24},{"sizeAppend":"MD","width":"MD","productCount":55},{"sizeAppend":"S","width":"S","productCount":24},{"sizeAppend":"W","width":"W","productCount":19},{"sizeAppend":"WIDE","width":"WIDE","productCount":7},{"sizeAppend":"XL","width":"XL","productCount":24},{"sizeAppend":"N","width":"N","productCount":9},{"sizeAppend":"R","width":"R","productCount":7}]'; 1630 var forcing = false; 1631 1632 // Known brand names for extraction from search queries 1633 const KNOWN_BRANDS = [ 1634 'brooks', 'nike', 'adidas', 'asics', 'saucony', 'hoka', 'new balance', 1635 'mizuno', 'altra', 'on', 'salomon', 'merrell', 'inov-8', 'topo athletic', 1636 'newton', 'karhu', 'under armour', 'reebok', 'puma', 'skechers', 1637 'la sportiva', 'diadora', 'oofos', 'balega', 'feetures', 'darn tough', 1638 'smartwool', 'swiftwick', 'bombas', 'thorlo', 'injinji', 'stance', 1639 'garmin', 'polar', 'coros', 'suunto', 'gu', 'maurten', 'tailwind', 1640 'nuun', 'skratch', 'hammer', 'clif', 'honey stinger', 'nathan', 1641 'amphipod', 'camelbak', 'ultimate direction', 'osprey', 'orange mud', 1642 'flipbelt', 'spibelt', 'lululemon', 'athleta', 'patagonia', 'rabbit' 1643 ]; 1644
1645 // Import synonym maps from search-overlay for consistent search behavior 1646 // This is a simplified version - only includes the most common terms 1647const SEARCH_SYNONYMS = { 1648 // Footwear types 1649 'shoe': ['shoes', 'sneaker', 'sneakers'], 1650 'shoes': ['shoe', 'sneaker', 'sneakers'], 1651 'sneaker': ['sneakers', 'shoe', 'shoes'], 1652 'sneakers': ['sneaker', 'shoe', 'shoes'], 1653 'racing flat': ['racing flats', 'flat', 'flats', 'racer', 'racers'], 1654 'spike': ['spikes', 'track spike', 'track spikes'], 1655 1656 // Shoe characteristics (from your categories) 1657 'neutral': ['cushioned', 'cushion', 'mens neutral', 'wmns neutral'], 1658 'stability': ['support', 'supportive', 'motion control', 'mens stability', 'wmns stability'], 1659 'motion control': ['stability', 'support', 'mens motion control', 'wmns motion control'], 1660 'cushion': ['cushioned', 'neutral', 'plush', 'soft'], 1661 'cushioned': ['cushion', 'neutral', 'plush', 'soft'], 1662 'minimal': ['minimalist', 'barefoot', 'zero drop'], 1663 'minimalist': ['minimal', 'barefoot', 'zero drop'], 1664 'maximal': ['maximalist', 'max cushion', 'ultra cushion'], 1665 'maximalist': ['maximal', 'max cushion', 'ultra cushion'], 1666 'responsive': ['bouncy', 'springy', 'energetic'], 1667 'lightweight': ['light', 'fast', 'racing'], 1668 'waterproof': ['water resistant', 'gore-tex', 'gtx'], 1669 'racing': ['race', 'fast', 'mens racing', 'wmns racing'], 1670 'trail': ['trails', 'hiking', 'mens trail', 'wmns trail'], 1671 'sandals': ['sandal', 'mens sandals', 'wmns sandals'], 1672 1673 // Gender terms (from your categories) 1674 'mens': ["men's", 'men', 'male', 'mns', 'm'], 1675 'men': ["men's", 'mens', 'male', 'mns', 'm'], 1676 'wmns': ["women's", 'womens', 'women', 'female', 'w'], 1677 'womens': ["women's", 'wmns', 'women', 'female', 'ladies', 'w'], 1678 'women': ["women's", 'womens', 'wmns', 'female', 'ladies', 'w'], 1679 'ladies': ["women's", 'women', 'womens', 'wmns', 'female', 'w'], 1680 'mns': ["men's", 'mens', 'men', 'male', 'm'], 1681 'm': ["men's", 'mens', 'men', 'male', 'mns'], 1682 'w': ["women's", 'womens', 'women', 'female', 'wmns'], 1683 'kids': ['youth', 'children', 'junior', 'boys', 'girls'], 1684 'youth': ['kids', 'children', 'junior'], 1685 1686 // Apparel types (from your categories) 1687 'sock': ['socks', 'hosiery'], 1688 'socks': ['sock', 'hosiery'], 1689 'short': ['shorts', '4" short', '5" short', '7" short', '9" short', 'split short'], 1690 'shorts': ['short', 'mens short', 'wmns short', 'long short'], 1691 'shirt': ['shirts', 'top', 'tops', 'tee', 'tees'], 1692 'shirts': ['shirt', 'top', 'tops', 'tee', 'tees'], 1693 'top': ['tops', 'shirt', 'shirts', 'tee', 'tees'], 1694 'tops': ['top', 'shirt', 'shirts', 'tee', 'tees', 'ss tops', 'ls tops'], 1695 'tee': ['tees', 'shirt', 'shirts', 't-shirt'], 1696 'tees': ['tee', 'shirt', 'shirts', 't-shirts'], 1697 'pant': ['pants', 'trouser', 'trousers', 'tight', 'tights', 'mens pant', 'wmns pant'], 1698 'pants': ['pant', 'trouser', 'trousers', 'tight', 'tights'], 1699 'tight': ['tights', 'legging', 'leggings', 'pant', 'pants', 'mens tight', 'wmns tight'], 1700 'tights': ['tight', 'legging', 'leggings', 'pant', 'pants'], 1701 'legging': ['leggings', 'tight', 'tights'], 1702 'leggings': ['legging', 'tight', 'tights'], 1703 'capri': ['capris', 'mens capri', 'wmns capri'], 1704 'jacket': ['jackets', 'coat', 'coats', 'outerwear'], 1705 'jackets': ['jacket', 'coat', 'coats', 'outerwear'], 1706 'outerwear': ['jacket', 'jackets', 'coat', 'coats', 'mens outerwear', 'wmns outerwear'], 1707 'zip': ['zipline', '1/2 zip', 'half zip', 'mens 1/2 zip', 'wmns 1/2 zip'], 1708 'bra': ['bras', 'sports bra', 'run bra', 'wmns bra'], 1709 'tank': ['tanks', 'singlet', 'mens tank', 'wmns tank'], 1710 'vest': ['vests', 'hydration vest', 'running vest'], 1711 'base layer': ['base layers', 'baselayer', 'thermal'], 1712 'arm sleeve': ['arm sleeves', 'sleeves'], 1713 'calf sleeve': ['calf sleeves', 'compression sleeve'], 1714 'long sleeve': ['ls', 'long sleeves', 'mens ls tops', 'wmns ls tops'], 1715 'short sleeve': ['ss', 'short sleeves', 'mens ss tops', 'wmns ss tops'], 1716 'skirt': ['skirts', 'wmns skirt', 'running skirt'], 1717 'dress': ['dresses'], 1718 'underwear': ['undergarments', 'mens underwear', 'wmns underwear'], 1719 '2 in 1': ['2-n-1', 'mens 2 in 1', 'wmns 2-n-1'], 1720 1721 // Accessories (from your categories) 1722 'glove': ['gloves', 'mitt', 'mitts'], 1723 'gloves': ['glove', 'mitt', 'mitts'], 1724 'hat': ['hats', 'cap', 'caps', 'beanie', 'headwear'], 1725 'cap': ['caps', 'hat', 'hats'], 1726 'visor': ['visors'], 1727 'headband': ['headbands', 'sweatband'], 1728 'buff': ['buffs', 'neck gaiter', 'gaiter'], 1729 'watch': ['watches', 'gps', 'fitness tracker', 'smartwatch', 'electronics'], 1730 'belt': ['belts', 'running belt', 'waist pack'], 1731 'pack': ['packs', 'hydration pack', 'vest', 'backpack', 'bags'], 1732 'bag': ['bags', 'backpack', 'duffel', 'pack'], 1733 'bottle': ['bottles', 'water bottle', 'flask', 'hydration'], 1734 'light': ['lights', 'headlamp', 'flashlight'], 1735 'reflective': ['reflective gear', 'visibility', 'hi-vis'], 1736 'sunglasses': ['glasses', 'shades', 'eyewear'], 1737 'eyewear': ['sunglasses', 'glasses', 'shades'], 1738 'insole': ['insoles', 'insert', 'inserts', 'footbed', 'shoe accessories'], 1739 'insoles': ['insole', 'insert', 'inserts', 'footbed'], 1740 'collar': ['collars', 'dog collar'], 1741 'stroller': ['strollers', 'jogging stroller'], 1742 'book': ['books', 'magazines', 'books/magazines'], 1743 'injury prevention': ['recovery', 'massage', 'foam roller'], 1744 1745 // Activity types (running specific) 1746 'run': ['running', 'runner', 'runners', 'jog', 'jogging'], 1747 'running': ['run', 'runner', 'runners', 'jog', 'jogging'], 1748 'jog': ['jogging', 'jogger', 'run', 'running'], 1749 'jogging': ['jog', 'jogger', 'run', 'running'], 1750 'walk': ['walking', 'walker'], 1751 'walking': ['walk', 'walker'], 1752 'hike': ['hiking', 'hiker', 'trail'], 1753 'hiking': ['hike', 'hiker', 'trail'], 1754 'trail': ['trails', 'hiking', 'hike', 'off-road', 'mountain', 'mens trail', 'wmns trail'], 1755 'ultra': ['ultramarathon', 'ultrarunning', 'ultra running'], 1756 'marathon': ['marathons', 'long distance', 'distance'], 1757 'half marathon': ['half', 'half marathons', '13.1'], 1758 '5k': ['5km', 'five k'], 1759 '10k': ['10km', 'ten k'], 1760 'track': ['racing', 'race', 'tempo'], 1761 'race': ['racing', 'track', 'tempo', 'speed'], 1762 'tempo': ['threshold', 'tempo run'], 1763 'interval': ['intervals', 'speed work', 'track work'], 1764 'recovery': ['recover', 'rest', 'easy', 'massage'], 1765 'training': ['train', 'trainer', 'workout', 'mens training', 'wmns training'], 1766 'lifestyle': ['casual', 'everyday', 'wmns lifestyle'], 1767 'tri': ['triathlon', 'wmns tri top'], 1768 1769 // Common terms 1770 'sport': ['sports', 'athletic'], 1771 'sports': ['sport', 'athletic'], 1772 'athletic': ['athletics', 'sport', 'sports'], 1773 'training': ['train', 'trainer', 'workout'], 1774 'workout': ['training', 'exercise', 'fitness'], 1775 'gear': ['equipment', 'accessories'], 1776 'compression': ['compress', 'tight', 'support', 'wmns compression'], 1777 'breathable': ['moisture wicking', 'wicking', 'ventilated'], 1778 'nutrition': ['fuel', 'energy', 'hydration', 'gu', 'gels'], 1779 'hydration': ['water', 'fluid', 'drink', 'nutrition', 'bottle'], 1780 'gel': ['gels', 'energy gel', 'gu'], 1781 'chew': ['chews', 'energy chew', 'blocks'], 1782 'clearance': ['sale', 'discount', 'closeout'], 1783 'apparel': ['clothing', 'clothes', 'mens apparel', 'womens apparel', 'kids apparel'], 1784 'footwear': ['shoes', 'shoe', 'mens footwear', 'womens footwear', 'kids footwear', 'unisex footwear'], 1785 1786 // Brand names (from your database) 1787 'hoka': ['hoka one one'], 1788 'nb': ['new balance'], 1789 'newbalance': ['new balance', 'nb'], 1790 'on': ['on running', 'on cloud'], 1791 1792 // Common brand misspellings - Running Shoe Brands 1793 'nike': ['nke', 'nikey', 'nyke', 'nikee'], 1794 'adidas': ['addidas', 'adiddas', 'addias', 'adidias', 'adiddis'], 1795 'balance': ['balence', 'ballance', 'balanse'], 1796 'new balance': ['newbalance', 'new ballance', 'new balence', 'newbalence'], 1797 'brooks': ['brook', 'broks', 'brookes', 'brookz', 'brroks'], 1798 'asics': ['asic', 'asix', 'asicks', 'acics', 'assics', 'asiks'], 1799 'saucony': ['sauconey', 'sacony', 'sauconny', 'saucany', 'socony', 'saucon'], 1800 'mizuno': ['mizino', 'mizumo', 'mizzuno', 'mizunno', 'misuno'], 1801 'altra': ['altrah', 'altera', 'alltra', 'alttra'], 1802 'salomon': ['salmon', 'salamon', 'salamon', 'soloman', 'solamon'], 1803 'hoka': ['hooka', 'hokka', 'hocka', 'hokah', 'hoko'], 1804 'merrell': ['merell', 'merrel', 'merel', 'merrill', 'merill'], 1805 'skechers': ['sketchers', 'skecher', 'sketcher', 'skeechers', 'skethers'], 1806 'puma': ['pumma', 'pooma', 'pumah'], 1807 'reebok': ['rebok', 'reebok', 'rebock', 'reebook'], 1808 'under armour': ['under armor', 'underarmour', 'underarmor', 'ua'], 1809 'oofos': ['oofoos', 'oofo', 'oofus', 'ooofos'], 1810 'topo athletic': ['topo', 'toppo', 'topo atheltic'], 1811 'inov-8': ['inov8', 'inov', 'innovate', 'innov8'], 1812 'karhu': ['karho', 'karuh', 'karoo'], 1813 'newton': ['newten', 'neuton'], 1814 'diadora': ['diadoria', 'diodora', 'diadorra'], 1815 'la sportiva': ['lasportiva', 'la sportive', 'sportiva'],
1816 'norda': ['norda', 'nordah'], 1817 1818 // Common product misspellings - Brooks 1819 'ghost': ['ghot', 'gost', 'ghoast', 'goast'], 1820 'adrenaline': ['adrenalin', 'adreneline', 'addrenaline', 'adrenalene', 'andenaline'], 1821 'glycerin': ['glycerine', 'glicerin', 'glyceran', 'glyserin', 'glycren'], 1822 'cascadia': ['cascada', 'cascadea', 'cascaddia', 'cascaida'], 1823 'levitate': ['levitae', 'levitat', 'levatate', 'levitte'], 1824 'hyperion': ['hypereon', 'hyperian', 'hyerion', 'hyperyon'], 1825 1826 // Common product misspellings - Nike 1827 'pegasus': ['pegusus', 'pegeses', 'pegasas', 'pegsus', 'pegisis', 'pegusas'], 1828 'vomero': ['vomaro', 'vommero', 'vomorro', 'vomera'], 1829 'vaporfly': ['vapourfly', 'vaporflye', 'vaperfly', 'vaporflie'], 1830 'alphafly': ['alphaflye', 'alfafly', 'alphaflie', 'alpahfly'], 1831 'invincible': ['invincable', 'invicible', 'invinsible', 'invincble'], 1832 'infinity': ['infiniti', 'infnity', 'infinty', 'infitity'], 1833 1834 // Common product misspellings - Hoka 1835 'bondi': ['bondie', 'bondy', 'bondii', 'bandi', 'bondee'], 1836 'clifton': ['cliffton', 'cliften', 'clifften', 'cliftton', 'cliftonn'], 1837 'speedgoat': ['speed goat', 'speedgot', 'speedgoaat', 'speegoat'], 1838 'rincon': ['rinconn', 'ricon', 'rinkin', 'rickon'], 1839 'arahi': ['arhi', 'arhai', 'arhi', 'ahrai'], 1840 'mach': ['mac', 'match', 'maach'], 1841 'gaviota': ['gaviotta', 'gavioat', 'gaviots', 'gavioota'], 1842 'kawana': ['kawanna', 'kowona', 'kawona', 'kawanna'], 1843 1844 // Common product misspellings - ASICS 1845 'kayano': ['kayanno', 'kayanna', 'kyano', 'kayno', 'kaiano'], 1846 'nimbus': ['nimbes', 'nimbis', 'nimbuus', 'nimbes'], 1847 'cumulus': ['cumulis', 'cumulous', 'cumulas', 'cummulus'], 1848 'novablast': ['nova blast', 'novablst', 'novvablast'], 1849 'metaspeed': ['meta speed', 'metaspede', 'metasped'], 1850 1851 // Common product misspellings - Saucony 1852 'kinvara': ['kinvarra', 'kinvera', 'kinvarra', 'kinvar', 'kinvra'], 1853 'triumph': ['truimph', 'triump', 'triumf', 'triuph', 'trumph'], 1854 'endorphin': ['endorfin', 'endorphan', 'endophin', 'endorphine'], 1855 'peregrine': ['peragrine', 'perregrine', 'perergrine', 'perigrine'], 1856 'hurricane': ['hurrican', 'huricane', 'hurricaine', 'huricain'], 1857 1858 // Common product misspellings - New Balance 1859 'fuelcell': ['fuel cell', 'fuelcel', 'fuellcell', 'fuelsel'], 1860 'fresh foam': ['freshfoam', 'fresh fome', 'freshfome'], 1861 1862 // Common product misspellings - Altra 1863 'escalante': ['escalant', 'escallante', 'escalente', 'escalanty'], 1864 'torin': ['torrin', 'toren', 'torinn', 'torren'], 1865 'lone peak': ['lonepeak', 'lone peek', 'lonepeek'], 1866 'timp': ['timmp', 'tymp', 'tem'], 1867 'paradigm': ['paradigim', 'peradigm', 'paradime', 'paradgm'], 1868 'olympus': ['olypmus', 'olympis', 'olumpus', 'olympas'], 1869 1870 // Common product misspellings - On Running 1871 'cloudrunner': ['cloud runner', 'cloudruner', 'cloudrunnr'], 1872 'cloudflow': ['cloud flow', 'cloudfloe', 'cloudflo'], 1873 'cloudmonster': ['cloud monster', 'cloudmonstr', 'cloudmoster'], 1874 'cloudsurfer': ['cloud surfer', 'cloudsurfer', 'cloudsurffer'], 1875 1876 // Common product misspellings - Mizuno 1877 'wave rider': ['waverider', 'wave ridder', 'waveridder'], 1878 'wave inspire': ['waveinspire', 'wave insipre', 'wave inspir'], 1879 1880 // Common product misspellings - Salomon 1881 'speedcross': ['speed cross', 'speedcros', 'speedcross'], 1882 1883 // Sock brand misspellings 1884 'feetures': ['features', 'feeture', 'featurs', 'feeturs', 'feechers'], 1885 'balega': ['balaga', 'balego', 'baleca', 'belega', 'bellega'], 1886 'darn tough': ['darntough', 'darn tuff', 'darntuff', 'darn tuf'], 1887 'smartwool': ['smart wool', 'smartwhool', 'smartwol', 'smartwoool'], 1888 'swiftwick': ['swiftwhick', 'swiftwik', 'swiftwhik', 'swift wick'], 1889 'injinji': ['injini', 'injinji', 'injiji', 'injenji', 'injinjii'], 1890 'thorlo': ['thorlow', 'thorloo', 'tharlo', 'thorlos'], 1891 'bombas': ['bomba', 'bombass', 'bombis', 'bomas'], 1892 1893 // Watch/tech brand misspellings 1894 'garmin': ['garmen', 'garmim', 'garman', 'gramin', 'garmon'], 1895 'polar': ['poler', 'pollar', 'polor'], 1896 'coros': ['corros', 'coras', 'corros', 'koros'], 1897 'suunto': ['sunto', 'suunto', 'sunnto', 'suunton', 'sunnto'],
1898 'whoop': ['woop', 'whooop', 'whopp'], 1899 'wahoo': ['wahho', 'waho', 'waahoo', 'wahoe'], 1900 1901 // Nutrition brand misspellings 1902 'maurten': ['morten', 'maurteen', 'marten', 'maurtenn', 'maurtun'], 1903 'nuun': ['nun', 'noon', 'nunn', 'nune'], 1904 'tailwind': ['tailwing', 'tailwinnd', 'tail wind', 'talwind'], 1905 'skratch': ['scratch', 'skrach', 'scrach', 'skratch'], 1906 'gu': ['guu', 'goo'], 1907 'clif': ['cliff', 'clif', 'clifff'], 1908 'honey stinger': ['honeystinger', 'honey stiner', 'hony stinger'], 1909 'huma': ['humma', 'humas', 'huuma'], 1910 'ucan': ['youcan', 'u can', 'uucan'], 1911 1912 // Hydration/accessory brand misspellings 1913 'nathan': ['nathen', 'nathann', 'nathon'], 1914 'amphipod': ['amphipod', 'amfipod', 'amphapod', 'amphypod'], 1915 'camelbak': ['camelback', 'camelbac', 'cammelback', 'camelbakk'], 1916 'ultimate direction': ['ultimatedirection', 'ultimate directon', 'ud'], 1917 'flipbelt': ['flip belt', 'flipblt', 'flibelt'], 1918 'spibelt': ['spy belt', 'spi belt', 'spybelt'], 1919 'osprey': ['ospry', 'osprery', 'ossprey'], 1920 1921 // Apparel brand misspellings 1922 'lululemon': ['lulu lemon', 'lulemon', 'lulullemon', 'lulumon'], 1923 'athleta': ['athletia', 'atheleta', 'athlete', 'atheleta'], 1924 'patagonia': ['patogonia', 'patigonia', 'patagona', 'patgonia'], 1925 'oiselle': ['oisele', 'oisselle', 'oisell', 'oisele'], 1926 'janji': ['janji', 'janjii', 'janjy', 'janji'], 1927 'vuori': ['vuoori', 'vouri', 'vuorii', 'voori'], 1928 'rhone': ['rhon', 'rone', 'rhonne'], 1929 1930 // Eyewear brand misspellings 1931 'goodr': ['gooder', 'goodrr', 'good r', 'goodar'], 1932 'oakley': ['oakly', 'okley', 'oakely', 'oaklee'], 1933 'tifosi': ['tifossi', 'tiffosi', 'tifos', 'tifosy'], 1934 }; 1935 1936 /** 1937 * Extract quoted strings from search query 1938 * Returns object with quoted phrases and cleaned query 1939 * Example: 'nike "wave inspire 16" shoes' => { quotedPhrases: ['wave inspire 16'], cleanedQuery: 'nike shoes' } 1940 * @param {string} query - The search query 1941 * @returns {Object} - { quotedPhrases: string[], cleanedQuery: string } 1942 */ 1943 function extractQuotedStrings(query) { 1944 const quotedPhrases = []; 1945 let cleanedQuery = query?.toString() || ''; 1946 1947 const quotePattern = /"([^"]*)"|'([^']*)'|"([^"]*)"|'([^']*)'/g; 1948 let match; 1949 1950 while ((match = quotePattern.exec(query)) !== null) { 1951 // Extract the matched text from whichever group captured it 1952 const quotedText = (match[1] || match[2] || match[3] || match[4] || '').trim(); 1953 if (quotedText.length > 0) { 1954 quotedPhrases.push(quotedText); 1955 // Remove the quoted phrase from the query (including quotes) 1956 cleanedQuery = cleanedQuery.replace(match[0], ' ').trim(); 1957 } 1958 } 1959 1960 // Clean up multiple spaces 1961 try { 1962 cleanedQuery = cleanedQuery.replace(/\s+/g, ' ').trim(); 1963 } catch (e) { 1964 1965 } 1966 1967 return { 1968 quotedPhrases, 1969 cleanedQuery 1970 }; 1971 } 1972 1973 // Extract brand names from search query 1974 function extractBrandsFromQuery(query, allBrands) { 1975 const normalized = query.toLowerCase().trim(); 1976 const words = normalized.split(/\s+/); 1977 const detectedBrands = []; 1978 const wordsToKeep = []; 1979 1980 // Build a brand map from allBrands array 1981 const brandMap = {}; 1982 if (allBrands && allBrands.length > 0) { 1983 allBrands.forEach(brand => { 1984 brandMap[brand.name.toLowerCase()] = brand.brandId; 1985 }); 1986 } 1987 1988 // Check each word (and combinations) against known brands 1989 let i = 0; 1990 while (i < words.length) { 1991 let matchFound = false; 1992 let skipWords = 0; 1993 1994 // Try 3-word combinations 1995 if (i + 2 < words.length) { 1996 const threeWord = `${words[i]} ${words[i+1]} ${words[i+2]}`; 1997 if (KNOWN_BRANDS.includes(threeWord)) { 1998 const brandId = brandMap[threeWord]; // May be undefined if brands not loaded yet 1999 detectedBrands.push({ id: brandId, name: threeWord }); 2000 skipWords = 3; 2001 matchFound = true; 2002 } 2003 } 2004 2005 // Try 2-word combinations 2006 if (!matchFound && i + 1 < words.length) { 2007 const twoWord = `${words[i]} ${words[i+1]}`; 2008 if (KNOWN_BRANDS.includes(twoWord)) { 2009 const brandId = brandMap[twoWord]; // May be undefined if brands not loaded yet 2010 detectedBrands.push({ id: brandId, name: twoWord }); 2011 skipWords = 2; 2012 matchFound = true; 2013 } 2014 } 2015 2016 // Try single word 2017 if (!matchFound) { 2018 const word = words[i]; 2019 if (KNOWN_BRANDS.includes(word)) { 2020 const brandId = brandMap[word]; // May be undefined if brands not loaded yet 2021 detectedBrands.push({ id: brandId, name: word }); 2022 skipWords = 1; 2023 matchFound = true; 2024 } 2025 } 2026 2027 if (matchFound) { 2028 i += skipWords; 2029 } else { 2030 wordsToKeep.push(words[i]); 2031 i++; 2032 } 2033 } 2034 2035 return { 2036 detectedBrands, 2037 cleanedQuery: wordsToKeep.join(' ') 2038 }; 2039 } 2040 2041 // Function to expand search terms with synonyms (matching search-overlay logic) 2042 function expandSearchTermsWithSynonyms(searchTerms) { 2043 // IMPORTANT: Start with the full original phrase to ensure phrase matching 2044 // Example: ["inspire", "15"] becomes ["inspire 15", "inspire", "15", ...] 2045 const allTerms = [searchTerms.join(' ')]; // Full phrase as first term 2046
2047 searchTerms.forEach(term => { 2048 const normalized = term.toLowerCase().trim(); 2049 2050 // Add original term only if not already present (avoid duplicates) 2051 if (!allTerms.includes(normalized)) { 2052 allTerms.push(normalized); 2053 } 2054 2055 // Add ALL synonyms if they exist (matching search-overlay behavior) 2056 if (SEARCH_SYNONYMS[normalized]) { 2057 SEARCH_SYNONYMS[normalized].forEach(syn => { 2058 if (!allTerms.includes(syn)) { 2059 allTerms.push(syn); 2060 } 2061 }); 2062 } 2063 }); 2064 2065 return allTerms; 2066 } 2067 2068 var page = { 2069 category: null, 2070 lastSearch: "", 2071 categories: [], 2072 brands: [], 2073 collections: [], 2074 products: [], 2075 popular: [], 2076 brandsCalled: false, 2077 doAfterCatLoad: false, 2078 doAfterBrandLoad: false, 2079 parentSelected: -1, 2080 isInitial: false, 2081 forceBrand: "", 2082 forceBrandId: -1, 2083 forceCategory: "", 2084 forceCategoryId: -1, 2085 terms: { 2086 instore: 0, 2087 shipable: 0, 2088 brand: { 2089 label: "", 2090 id: -1 2091 }, 2092 categories: { 2093 label: "", 2094 id: -1 2095 }, 2096 filters: [], 2097 filter: "", 2098 sizes: [], 2099 widths: [], 2100 prices: [] 2101 }, 2102 /** 2103 * Extract gender keywords from query and return query with gender codes 2104 * NOTE: We don't remove gender keywords from the query - they still help with matching 2105 * @param {string} query - The search query 2106 * @returns {Object} - { cleanedQuery: string, genders: string[] } 2107 */ 2108 extractGenderFromQuery: function(query) { 2109 // Gender keyword patterns mapped to gender codes 2110 const genderPatterns = { 2111 'M': [ 2112 /\b(men'?s?|male|mns?|boys?)\b/gi 2113 ], 2114 'W': [ 2115 /\b(women'?s?|womens?|wmns|female|ladies|girls?)\b/gi 2116 ], 2117 'U': [ 2118 /\bunisex\b/gi 2119 ], 2120 'K': [ 2121 /\b(kids?|youth|children|child)\b/gi 2122 ] 2123 }; 2124 2125 const detectedGenders = new Set(); 2126 2127 // Check each gender pattern but DON'T remove from query 2128 for (const [genderCode, patterns] of Object.entries(genderPatterns)) { 2129 for (const pattern of patterns) { 2130 if (pattern.test(query)) { 2131 detectedGenders.add(genderCode); 2132 } 2133 } 2134 } 2135 2136 return { 2137 cleanedQuery: query, // Keep original query intact 2138 genders: Array.from(detectedGenders) 2139 }; 2140 }, 2141 /** 2142 * Extract color keywords from query and return detected colors 2143 * @param {string} query - The search query 2144 * @returns {Object} - { cleanedQuery: string, colors: string[] } 2145 */ 2146 extractColorFromQuery: function(query) { 2147 // Color keyword patterns mapped to color names (matching colorDefinitions) 2148 const colorPatterns = { 2149 'Red': /\b(red|crimson|scarlet|maroon|burgundy)\b/gi, 2150 'Orange': /\b(orange|tangerine|coral)\b/gi, 2151 'Yellow': /\b(yellow|gold|golden|mustard|lemon)\b/gi, 2152 'Green': /\b(green|olive|lime|mint|teal|emerald|forest)\b/gi, 2153 'Blue': /\b(blue|navy|aqua|cyan|cobalt|royal|indigo|turquoise)\b/gi, 2154 'Purple': /\b(purple|violet|lavender|plum|magenta)\b/gi, 2155 'Pink': /\b(pink|rose|salmon|fuchsia|blush)\b/gi, 2156 'Beige': /\b(beige|tan|khaki|cream|nude|sand)\b/gi, 2157 'Brown': /\b(brown|chocolate|coffee|mocha|caramel|chestnut)\b/gi, 2158 'Black': /\b(black|onyx|charcoal|ebony)\b/gi, 2159 'Gray': /\b(gray|grey|silver|slate|graphite|ash)\b/gi, 2160 'White': /\b(white|ivory|pearl|snow|cream)\b/gi 2161 }; 2162 2163 const detectedColors = new Set(); 2164 let cleanedQuery = query; 2165 2166 // Check each color pattern and remove from query 2167 for (const [colorName, pattern] of Object.entries(colorPatterns)) { 2168 if (pattern.test(query)) { 2169 detectedColors.add(colorName); 2170 // Remove the color keyword from query to avoid redundant search 2171 cleanedQuery = cleanedQuery.replace(pattern, ' '); 2172 } 2173 } 2174 2175 // Normalize whitespace 2176 cleanedQuery = cleanedQuery.trim().replace(/\s+/g, ' '); 2177 2178 return { 2179 cleanedQuery: cleanedQuery, 2180 colors: Array.from(detectedColors) 2181 }; 2182 }, 2183 init: function () { 2184 $("#PagingTotalsNone").hide(); 2185 if (document.referrer) { 2186 var url = document.referrer; 2187 var ref = url.match(/:\/\/(.[^/]+)/)[1]; 2188 if (ref!=window.location.hostname) { 2189 page.terms = { brand: { label: "", id: -1 }, brands: [], collections: [], categories: { label: "", id: -1 }, filter: "", filters: [], quotedFilters: [], sizes: [], widths: [], prices: [], colors: [], genders: [], page: 1 }; 2190 context.set("storeTerms", page.terms); 2191 } 2192 } else { 2193 //page.terms = { brand: { label: "", id: -1 }, categories: { label: "", id: -1 }, filter: "", filters: [], sizes: [], widths: [] }; 2194 //context.set("storeTerms", page.terms); 2195 } 2196 2197 // Bug #4 fix: If navigating to /shop with NO filters (clean URL), clear saved filters 2198 // This ensures clicking "Shop" in nav resets to a fresh browse experience 2199 var isCleanShopUrl = (window.location.pathname === '/shop' || window.location.pathname === '/shop/') && 2200 window.location.hash.length === 0 && 2201 window.location.search.length === 0; 2202 if (isCleanShopUrl) { 2203 page.terms = { brand: { label: "", id: -1 }, brands: [], collections: [], categories: { label: "", id: -1 }, filter: "", filters: [], quotedFilters: [], sizes: [], widths: [], prices: [], colors: [], genders: [], page: 1 }; 2204 context.set("storeTerms", page.terms); 2205 } 2206 2207 if (window.location.pathname.length>1) { 2208 var parts = window.location.pathname.substr(1).split('/'); 2209 if (parts[0]=="brand") { 2210 forcing = true;
2211 if (isNaN(parts[1])) page.forceBrand = parts[1].split('%20').join(' '); 2212 else page.forceBrandId = Number(parts[1]); 2213 } 2214 if (parts[0]=="category") { 2215 forcing = true; 2216 if (isNaN(parts[1])) page.forceCategory = parts[parts.length-1].split('%20').join(' '); 2217 else page.forceCategoryId = Number(parts[1]); 2218 } 2219 } 2220 products.searchId = ""; //moment().unix(); 2221 if (!products.filter) products.filter = {}; 2222 products.filter.grouped = false; // Show ungrouped variant counts to match brand filter counts 2223 2224 // Check if there's a search query in the URL 2225 const urlParams = new URLSearchParams(window.location.search); 2226 const hasSearchParam = urlParams.has('search') || urlParams.has('q'); 2227 const hasSortParam = urlParams.has('sort'); 2228 const saleParamValue = (urlParams.get('sale') || '').trim().toLowerCase(); 2229 const hasSaleParam = saleParamValue === '1' || saleParamValue === 'true' || saleParamValue === 'yes'; 2230 2231 // Set default sort based on context 2232 if (defaultSort && defaultSort !== '') { 2233 var sortBy = defaultSort.split(" "); 2234 if (sortBy.length == 2) { 2235 products.filter.sort = sortBy[0]; 2236 products.filter.sortType = sortBy[1]; 2237 } 2238 $('#SortBy').val(defaultSort); 2239 } else if (hasSearchParam && !hasSortParam) { 2240 // When searching without explicit sort, default to relevance 2241 products.filter.sort = "relevance"; 2242 products.filter.sortType = "ASC"; 2243 $('#SortBy').val("relevance ASC"); 2244 } else { 2245 products.filter.sort = "brandName, name"; 2246 } 2247 page.events(); 2248 2249 // Initialize color filter 2250 page.initColors(); 2251 2252 // Load wishlist for logged-in users 2253 if (user && user.data && user.data.memberId && typeof wishlist !== 'undefined' && !wishlist.isLoaded) { 2254 wishlist.load(); 2255 } 2256 2257 brand.filter.size = 1000; 2258 brand.get(); // Load brands for filter 2259 2260 collection.filter.size = 1000; 2261 collection.get(); // Load collections for filter 2262 2263 // Load sizes and widths for filters 2264 page.sizesLoaded(page.parseValueList(proSizes)); 2265 page.widthsLoaded(page.parseValueList(proWidths)); 2266 2267 // Initialize price filter checkboxes (will show 0 counts until products load) 2268 page.initializePriceFilters(); 2269 2270 // Check for query string params BEFORE restoring storeTerms - they should override saved filters 2271 if (window.location.search.length > 0) { 2272 if (urlParams.has('brand') || urlParams.has('category') || urlParams.has('search') || urlParams.has('q') || hasSaleParam) { 2273 forcing = true; // Set forcing early so storeTerms are not restored 2274 page.isInitial = false; // Ensure API call is made since server-rendered products may not match filters 2275 } 2276 } 2277 2278 var lastFilter = context.get("storeTerms"); 2279 if (lastFilter&&!forcing&&((lastFilter.filters&&lastFilter.filters.length>0) || lastFilter.categories.id>-1 || (lastFilter.brands&&lastFilter.brands.length>0))) { 2280 page.terms = lastFilter; 2281 // Migrate old brand structure to new brands array if needed 2282 if (!page.terms.brands) { 2283 page.terms.brands = []; 2284 if (page.terms.brand && page.terms.brand.id !== -1) { 2285 page.terms.brands.push({ 2286 id: page.terms.brand.id, 2287 label: page.terms.brand.label 2288 }); 2289 } 2290 } 2291 // Ensure the old singular brand object is cleared if brands array is empty 2292 if (page.terms.brands.length === 0) { 2293 page.terms.brand = { label: "", id: -1 }; 2294 } 2295 // Ensure colors array exists 2296 if (!page.terms.colors) { 2297 page.terms.colors = []; 2298 } 2299 // Ensure quotedFilters array exists (migration from old saved terms) 2300 if (!page.terms.quotedFilters) { 2301 page.terms.quotedFilters = []; 2302 } 2303 page.isInitial = false; 2304 } 2305 if (inStoreAllowed) $("#ShippingArea").show(); 2306 2307 // Bug #3 fix: Handle query string params (?search=ghost or ?q=ghost) directly
2308 // Process them the same way as urlParamsChanged, but keep original URL 2309 if (window.location.search.length > 0) { 2310 // Handle search param from query string (support both 'search' and 'q' params) 2311 var searchParam = urlParams.get('search') || urlParams.get('q'); 2312 if (searchParam && searchParam.trim().length > 0) { 2313 var searchValue = decodeURIComponent(searchParam.trim()); 2314 2315 // FIRST: Extract quoted strings before any other processing 2316 // Example: 'nike "wave inspire 16" shoes' => quotedPhrases: ['wave inspire 16'], cleanedQuery: 'nike shoes' 2317 var quoteResult = extractQuotedStrings(searchValue); 2318 var quotedPhrases = quoteResult.quotedPhrases; 2319 var cleanedSearch = quoteResult.cleanedQuery; 2320 2321 // Check if the entire search is quoted (no remaining text after quote extraction) 2322 var isFullyQuoted = quotedPhrases && quotedPhrases.length > 0 && (!cleanedSearch || cleanedSearch.trim().length === 0); 2323 2324 // Clear old filters for a fresh search (same as hashChanged does) 2325 page.terms.filters = []; 2326 page.terms.quotedFilters = []; // Track which filters came from quoted searches 2327 page.terms.genders = []; 2328 page.terms.brands = []; 2329 page.terms.collections = []; 2330 page.terms.sizes = []; 2331 page.terms.widths = []; 2332 page.terms.prices = []; 2333 page.terms.colors = []; 2334 2335 if (isFullyQuoted) { 2336 // Fully quoted search - use ONLY the quoted phrase(s), no brand/gender/color extraction 2337 quotedPhrases.forEach(function(phrase) { 2338 if (phrase && phrase.trim().length > 0 && !page.terms.filters.includes(phrase)) { 2339 page.terms.filters.push(phrase); 2340 page.terms.quotedFilters.push(phrase); // Mark as quoted 2341 } 2342 }); 2343 } else { 2344 // Normal search or mixed quoted+unquoted - process brands/genders/colors 2345 2346 // Add quoted phrases to filters first (if any) and mark them as quoted 2347 if (quotedPhrases && quotedPhrases.length > 0) { 2348 quotedPhrases.forEach(function(phrase) { 2349 if (phrase && phrase.trim().length > 0 && !page.terms.filters.includes(phrase)) { 2350 page.terms.filters.push(phrase); 2351 page.terms.quotedFilters.push(phrase); // Mark as quoted 2352 } 2353 }); 2354 } 2355 2356 // Extract gender from the remaining search query (after quotes removed) 2357 var genderExtraction = page.extractGenderFromQuery(cleanedSearch); 2358 cleanedSearch = cleanedSearch; 2359 if (genderExtraction.genders && genderExtraction.genders.length > 0) { 2360 if (!page.terms.genders) page.terms.genders = []; 2361 var genderLabels = { 'M': 'Mens', 'W': 'Womens', 'U': 'Unisex', 'K': 'Kids' }; 2362 genderExtraction.genders.forEach(function(g) { 2363 if (!page.terms.genders.includes(g)) { 2364 page.terms.genders.push(g); 2365 } 2366 }); 2367 // Remove gender keywords from search 2368 cleanedSearch = searchValue.replace(/\b(men'?s?|women'?s?|womens?|wmns|male|female|ladies|boys?|girls?|kids?|youth|children|child|unisex|mns?)\b/gi, ' '); 2369 cleanedSearch = cleanedSearch.trim().replace(/\s+/g, ' '); 2370 } 2371 2372 // Extract colors from the search query (e.g., "red shoes" â apply red color filter) 2373 var colorExtraction = page.extractColorFromQuery(cleanedSearch); 2374 if (colorExtraction.colors && colorExtraction.colors.length > 0) { 2375 if (!page.terms.colors) page.terms.colors = []; 2376 if (!page.terms.filters) page.terms.filters = []; 2377 colorExtraction.colors.forEach(function(colorName) { 2378 if (!page.terms.colors.some(function(c) { return c.name === colorName; })) { 2379 var colorDef = page.colorDefinitions ? page.colorDefinitions.find(function(c) { return c.name === colorName; }) : null; 2380 page.terms.colors.push({ 2381 name: colorName, 2382 hex: colorDef ? colorDef.hex : '#888888' 2383 }); 2384 // Add color to filters array so it's actually searched 2385 var colorLower = colorName.toLowerCase(); 2386 if (!page.terms.filters.includes(colorLower)) { 2387 page.terms.filters.push(colorLower); 2388 } 2389 // Check the corresponding color checkbox 2390 $('#ColorCheckboxList input[value="' + colorName + '"]').prop("checked", true); 2391 $('#MobileFilterContent #ColorCheckboxList input[value="' + colorName + '"]').prop("checked", true); 2392 } 2393 }); 2394 // Use the cleaned query (with color removed) for the search f
2394ilter 2395 cleanedSearch = colorExtraction.cleanedQuery; 2396 } 2397 2398 // Extract brands from the search term before adding to filters 2399 // Note: page.brands may not be loaded yet at this point, but we still extract 2400 // based on KNOWN_BRANDS and the brands will be applied once page.brands loads 2401 if (cleanedSearch && cleanedSearch.length > 0) { 2402 var brandExtraction = extractBrandsFromQuery(cleanedSearch, page.brands || []); 2403 2404 // If brands were detected, store their names for later mapping 2405 // We can't add IDs yet because page.brands isn't loaded 2406 if (brandExtraction.detectedBrands && brandExtraction.detectedBrands.length > 0) { 2407 if (!page.pendingBrandNames) page.pendingBrandNames = []; 2408 brandExtraction.detectedBrands.forEach(function(brand) { 2409 // If we got an ID (brands were already loaded), add to page.terms.brands 2410 if (brand.id) { 2411 if (!page.terms.brands) page.terms.brands = []; 2412 if (!page.terms.brands.some(b => b.id === brand.id)) { 2413 page.terms.brands.push({ 2414 id: brand.id, 2415 label: brand.name 2416 }); 2417 } 2418 } else { 2419 // Store brand name for later mapping when brands load 2420 if (!page.pendingBrandNames.includes(brand.name)) { 2421 page.pendingBrandNames.push(brand.name); 2422 } 2423 } 2424 }); 2425 } 2426 2427 // Add only the cleaned query (without brand names) to filters 2428 var finalSearchTerm = brandExtraction.cleanedQuery.trim(); 2429 if (finalSearchTerm.length > 0 && !page.terms.filters.includes(finalSearchTerm)) { 2430 page.terms.filters.push(finalSearchTerm); 2431 } 2432 } 2433 } // End of else block for non-fully-quoted searches 2434 2435 // Mark that we've already processed the search param to prevent urlParamsChanged from re-processing 2436 page.searchParamProcessed = true; 2437 2438 // Trigger pills rendering - but if brands need to load, brandsLoaded will handle it 2439 if (page.brands && page.brands.length > 0) { 2440 // Brands already loaded, safe to render pills now 2441 page.pills(); 2442 } 2443 // If brands aren't loaded yet, brandsLoaded will call page.pills() when ready 2444 2445 forcing = true; 2446 } 2447 2448 // Handle brand param from query string 2449 if (urlParams.has('brand') && urlParams.get('brand').trim().length > 0) { 2450 var brandValue = urlParams.get('brand').trim(); 2451 var brandId = parseInt(brandValue); 2452 if (!isNaN(brandId)) { 2453 page.forceBrandId = brandId; 2454 } else { 2455 page.forceBrand = decodeURIComponent(brandValue).split('%20').join(' '); 2456 } 2457 forcing = true; 2458 } 2459 2460 // Handle category param from query string 2461 if (urlParams.has('category') && urlParams.get('category').trim().length > 0) { 2462 var categoryValue = urlParams.get('category').trim(); 2463 var categoryId = parseInt(categoryValue); 2464 if (!isNaN(categoryId)) { 2465 page.forceCategoryId = categoryId; 2466 } else { 2467 page.forceCategory = decodeURIComponent(categoryValue).split('%20').join(' '); 2468 } 2469 forcing = true; 2470 } 2471 } 2472 2473 if (window.location.search.length > 0) { 2474 page.urlParamsChanged(); 2475 } else if (forcing) { 2476 // If we processed query params, trigger search 2477 // But if forceBrand is set (either name or ID), wait for brandsLoaded to add it to page.terms.brands 2478 // Similarly for forceCategory - wait for categoriesLoaded to resolve 2479 if (page.forceBrand != "" || page.forceBrandId > 0 || 2480 page.forceCategory != "" || page.forceCategoryId > 0) { 2481 // Don't call pills() yet - brandsLoaded/categoriesLoaded will handle it 2482 } else { 2483 //page.pills(); 2484 } 2485 } else { 2486 // No query params and no forcing - load default products 2487 if (!lastFilter || !lastFilter.filters || lastFilter.filters.length === 0) { 2488 //page.pills(); 2489 } 2490 } 2491 if (shipType == 'flat' && flatRate <= 0) { 2492 $(".threshold").removeClass("hidden"); 2493 } else { 2494 if (threshold != null && threshold >= 0) { 2495 $(".threshold").removeClass("hidden"); 2496 if (threshold > 0) { 2497 $(".freeterms").removeClass("hidden"); 2498 $("#Threshold").html(threshold.toFixed(2)); 2499 } 2500 } 2501 } 2502 if (typeof products.popular === 'function') { 2503 products.popular(); 2504 } 2505 page.resizeFilters(); 2506 2507 let totalPages = Number($("#SearchTotalPages").html()); 2508 let paging = ""; 2509 for (var i=1; i<=totalPages && i<=10; i++) { 2510 paging += '<div class="page'+((i==1)?" current":"")+'" data-page="'+i+'">'+i+'</div>'; 2511 } 2512 $(".pages").html(paging); 2513 $(".page").click(page.page); 2514 if (brandImage && brandImage.trim()!='' && brandImage.indexOf("NoImage.svg") == -1) { 2515 $("#BrandImage").prop("src", brandImage); 2516 $("#BrandingImage").show(); 2517 } 2518 if (totalPages<1) $("#PagingArea").hide(); 2519 page.sizesLoaded(page.parseValueList(proSizes)); 2520 page.widthsLoaded(page.parseValueList(proWidths)); 2521 page.initializePriceFilters();
2522 // page.pillsChanged(); 2523 }, 2524 parseValueList: (value) => { 2525 if (value === null || value === undefined) return []; 2526 if (Array.isArray(value)) return value; 2527 if (typeof value !== 'string') return [value]; 2528 const trimmed = value.trim(); 2529 if (trimmed.startsWith('[') && trimmed.endsWith(']')) { 2530 try { 2531 return JSON.parse(trimmed); 2532 } catch (e) {} 2533 } 2534 return trimmed.split(',').map(v => v.trim()).filter(v => v.length > 0); 2535 }, 2536 resizeFilters: function() { 2537 const width = $(window).width(); 2538 if (width < 992 && !page.mobile) { 2539 page.mobile = true; 2540 $(".mobile-filter-group").detach().insertBefore('#ProductList'); 2541 $(".paging-labels").detach().insertBefore('#ProductList'); 2542 // Add 'positioned' class to show paging labels after moving 2543 setTimeout(() => $(".paging-labels").addClass('positioned'), 10); 2544 // Don't move SortArea on mobile - it stays next to Filters button 2545 // $(".page-select-container").detach().insertAfter('#BrandArea'); 2546 $(".mobile-filter-group .widget").addClass('min-view-area'); 2547 page.toggleFilters({}, true); 2548 } else if (width >= 992 && page.mobile) { 2549 page.mobile = false; 2550 $(".mobile-filter-group").detach().insertBefore('#ReturnPolicy'); 2551 $(".search-widget").detach().insertBefore('#PillBoxArea'); 2552 $(".paging-labels").detach().insertBefore('.page-select-container'); 2553 $(".paging-labels").removeClass('positioned'); 2554 $(".search-widget").removeClass('min-view'); 2555 $(".paging-container").removeClass('min-view'); 2556 $(".search-page-container").removeClass('min-view'); 2557 $(".mobile-filter-group .widget").removeClass('min-view-area'); 2558 } 2559 }, 2560 events: function () { 2561 context.addListener("categories", page.categoriesLoaded); 2562 context.addListener("brands", page.brandsLoaded); 2563 context.addListener("collections", page.collectionsLoaded); 2564 context.addListener("popular", page.popularLoaded); 2565 // context.addListener("products"+products.searchId, page.productsLoaded); 2566 context.addListener("sizes", page.sizesLoaded); 2567 context.addListener("widths", page.widthsLoaded); 2568 context.addListener("wishlist", page.updateWishlistButtons); 2569 2570 // Check if categories are already loaded in context and trigger callback 2571 const existingCategories = context.get("categories"); 2572 if (existingCategories && existingCategories.length > 0) { 2573 page.categoriesLoaded(existingCategories); 2574 } 2575 2576 $('#FiltersExpand').click(page.toggleFilters); 2577 $('#PillBoxExpand').click(page.togglePills); 2578 $('#CategoryArea .filter-container').click(page.toggleCategories); 2579 $('#SortArea .filter-container').click(page.toggleSort); 2580 $('#BrandArea .filter-container').click(page.toggleBrands); 2581 $('#CollectionArea .filter-container').click(page.toggleCollections); 2582 $('#SizeArea .filter-container').click(page.toggleSizes); 2583 $('#WidthArea .filter-container').click(page.toggleWidths); 2584 $('#PriceArea .filter-container').click(page.togglePrice); 2585 $('#ColorArea .filter-container').click(page.toggleColor); 2586 $('#ShippingArea .filter-container').click(page.togglePickup); 2587 $('#ReturnPolicy .filter-container').click(page.toggleReturn); 2588 $('#PopularArea .filter-container').click(page.togglePopular); 2589 $(".checker").click(function(e) { 2590 if ($(e.currentTarget).hasClass("selected")) $(e.currentTarget).removeClass("selected"); 2591 else $(e.currentTarget).addClass("selected"); 2592 }); 2593 $("#ClearFilterButton").click(page.clear); 2594 $("#SortBy").change(page.sort); 2595 2596 // Setup options checkboxes to sync between desktop and mobile 2597 $("input[data-filter='pickup-in-store']").off('change').on('change', function() { 2598 const isChecked = $(this).is(':checked'); 2599 $("input[data-filter='pickup-in-store']").prop('checked', isChecked);
2600 page.pillsChanged(true); 2601 }); 2602 2603 $("input[data-filter='on-sale']").off('change').on('change', function() { 2604 const isChecked = $(this).is(':checked'); 2605 $("input[data-filter='on-sale']").prop('checked', isChecked); 2606 page.pillsChanged(true); 2607 }); 2608 2609 $("input[data-filter='ship-to-me']").off('change').on('change', function() { 2610 const isChecked = $(this).is(':checked'); 2611 $("input[data-filter='ship-to-me']").prop('checked', isChecked); 2612 page.pillsChanged(true); 2613 }); 2614 2615 // Listen for popstate (browser back/forward) to handle URL changes 2616 $(window).on('popstate', page.urlParamsChanged); 2617 2618 // Trigger urlParamsChanged on initial page load if query params exist 2619 if (window.location.search) { 2620 page.urlParamsChanged(); 2621 } 2622 2623 $(window).resize(function(event) { 2624 page.resizeFilters(); 2625 }); 2626 $("nav").find("a").click((e) => { 2627 page.terms = { brand: { label: "", id: -1 }, brands: [], collections: [], categories: { label: "", id: -1 }, filter: "", filters: [], quotedFilters: [], sizes: [], widths: [], colors: [], page: 1 }; 2628 context.set("storeTerms", page.terms); 2629 }); 2630 2631 // Mobile filter panel handlers 2632 $("#OpenMobileFilters").click(page.openMobileFilters); 2633 $("#CloseMobileFilters").click(page.closeMobileFilters); 2634 $("#MobileFilterOverlay").click(page.closeMobileFilters); 2635 $("#ApplyMobileFilters").click(page.applyMobileFilters); 2636 2637 // Initialize filter counts on page load 2638 setTimeout(function() { 2639 page.updateFilterCounts(); 2640 }, 500); 2641 }, 2642 mobileFiltersCloned: false, 2643 openMobileFilters: function() { 2644 // Clone filters to mobile panel on first open 2645 if (!page.mobileFiltersCloned) { 2646 // Clone just the FiltersList content (not the accordion wrapper or Sort By) 2647 const filtersClone = $("#FiltersList").clone(); 2648 filtersClone.show(); // Make sure it's visible 2649 filtersClone.find('.filter-container').removeClass('closed'); 2650 filtersClone.find('.toggleright').removeClass('ti-angle-up').addClass('ti-angle-down'); 2651 // Remove any Sort By elements that might have been cloned 2652 filtersClone.find('#SortArea, #SortBy').remove(); 2653 2654 // Prefix input/select IDs with "mobile-" and update label "for" attributes to match 2655 filtersClone.find('input[id], select[id]').each(function() { 2656 const oldId = $(this).attr('id'); 2657 if (oldId) { 2658 const newId = 'mobile-' + oldId; 2659 $(this).attr('id', newId); 2660 // Update any labels that reference this input 2661 filtersClone.find('label[for="' + oldId + '"]').attr('for', newId); 2662 } 2663 }); 2664 2665 $("#MobileFilterContent").html(filtersClone); 2666 page.mobileFiltersCloned = true; 2667 2668 // Re-attach event handlers for cloned filters 2669 $("#MobileFilterContent .filter-container").each(function() { 2670 const parentId = $(this).parent().attr('id'); 2671 if (parentId === 'CategoryArea') { 2672 $(this).off('click').on('click', function(e) { 2673 // Only toggle if clicking the header itself, not children 2674 if ($(e.target).closest('.category-link, .category-toggle, .checkboxlabel, input').length === 0) { 2675 page.toggleCategories.call(this, e); 2676 } 2677 }); 2678 } else if (parentId === 'BrandArea') { 2679 $(this).off('click').on('click', function(e) { 2680 if ($(e.target).closest('.checkboxlabel, input, a').length === 0) { 2681 page.toggleBrands.call(this, e); 2682 } 2683 }); 2684 } else if (parentId === 'CollectionArea') { 2685 $(this).off('click').on('click', function(e) { 2686 if ($(e.target).closest('.checkboxlabel, input, a').length === 0) { 2687 page.toggleCollections.call(this, e); 2688 } 2689 }); 2690 } else if (parentId === 'SizeArea') { 2691 $(this).off('click').on('click', function(e) { 2692 if ($(e.target).closest('.checkboxlabel, input, a').length === 0) { 2693 page.toggleSizes.call(this, e); 2694 } 2695 }); 2696 } else if (parentId === 'WidthArea') { 2697 $(this).off('click').on('click', function(e) { 2698 if ($(e.target).closest('.checkboxlabel, input, a').length === 0) { 2699 page.toggleWidths.call(this, e); 2700 } 2701 }); 2702 } else if (parentId === 'PriceArea') { 2703 $(this).off('click').on('click', function(e) { 2704 if ($(e.target).closest('input').length === 0) { 2705 page.togglePrice.call(this, e); 2706 } 2707 }); 2708 } else if (parentId === 'ColorArea') { 2709 $(this).off('click').on('click', function(e) { 2710 if ($(e.target).closest('.checkboxlabel, input, a').length === 0) { 2711 page.toggleColor.call(this, e); 2712 } 2713 }); 2714 } else if (parentId === 'ShippingArea') { 2715 $(this).off('click').on('click', function(e) { 2716 if ($(e.target).closest('.checkboxlabel, input').length === 0) { 2717 page.togglePickup.call(this, e); 2718 } 2719 }); 2720 } else if (parentId === 'PopularArea') { 2721 $(this).off('click').on('click', function(e) { 2722 if ($(e.target).closest('.checkboxlabel, input').length === 0) { 2723 page.togglePopular.call(this, e); 2724 } 2725 }); 2726 } 2727 }); 2728 2729 // Re-attach checkbox handlers
2730 $("#MobileFilterContent input[type='checkbox']").off("change").on("change", function() { 2731 const name = $(this).attr('name'); 2732 const value = $(this).val(); 2733 2734 if (name === 'brand-filter') { 2735 const brandId = parseInt(value); 2736 const brandLabel = $(this).siblings('.checkboxlabel').text().replace(/\s*\(\d+\)/, '').trim(); 2737 const isChecked = $(this).is(":checked"); 2738 2739 if (isChecked) { 2740 if (!page.terms.brands.some(b => b.id === brandId)) { 2741 page.terms.brands.push({ id: brandId, label: brandLabel }); 2742 } 2743 page.addPill(brandId, brandLabel, "brand"); 2744 } else { 2745 page.terms.brands = page.terms.brands.filter(b => b.id !== brandId); 2746 page.pills(); 2747 } 2748 2749 products.filter.brands = page.terms.brands.map(b => b.id); 2750 page.pillsChanged(); 2751 // Sync desktop checkbox to match mobile 2752 $("#FiltersList input[name='brand-filter'][value='" + value + "']").prop('checked', isChecked); 2753 } else if (name === 'size-filter') { 2754 const size = value; 2755 const isChecked = $(this).is(":checked"); 2756 2757 if (isChecked) { 2758 if (!page.terms.sizes.includes(size)) { 2759 page.terms.sizes.push(size); 2760 } 2761 page.addPill(size, size, "size"); 2762 } else { 2763 page.terms.sizes = page.terms.sizes.filter(s => s !== size); 2764 page.pills(); 2765 } 2766 2767 products.filter.sizes = page.terms.sizes; 2768 page.pillsChanged(); 2769 // Sync desktop checkbox to match mobile 2770 $("#FiltersList #SizeCheckboxList input[value='" + size + "']").prop("checked", isChecked); 2771 } else if (name === 'width-filter') { 2772 const width = value; 2773 const isChecked = $(this).is(":checked"); 2774 2775 if (isChecked) { 2776 if (!page.terms.widths.includes(width)) { 2777 page.terms.widths.push(width); 2778 } 2779 page.addPill(width, width, "width"); 2780 } else { 2781 page.terms.widths = page.terms.widths.filter(w => w !== width); 2782 page.pills(); 2783 } 2784 2785 products.filter.widths = page.terms.widths; 2786 page.pillsChanged(); 2787 // Sync desktop checkbox to match mobile 2788 $("#FiltersList #WidthCheckboxList input[value='" + width + "']").prop("checked", isChecked); 2789 } else if (name === 'color-filter') { 2790 const colorName = value; 2791 const isChecked = $(this).is(":checked"); 2792 2793 if (isChecked) { 2794 // Trigger NLP-based AI search for the color using lowercase (this handles pill creation) 2795 page.addSearch(colorName.toLowerCase(), null, 'filter'); 2796 } else { 2797 // Remove the filter when unchecked 2798 page.terms.filters = page.terms.filters.filter(f => f.toLowerCase() !== colorName.toLowerCase()); 2799 page.pills(); 2800 page.pillsChanged(); 2801 } 2802 2803 // Sync desktop checkbox to match mobile 2804 $("#FiltersList #ColorCheckboxList input[value='" + colorName + "']").prop('checked', isChecked); 2805 } else if (name === 'collection-filter') { 2806 const collectionId = parseInt(value); 2807 const collectionTitle = $(this).data("collection-title"); 2808 const isChecked = $(this).is(":checked"); 2809 2810 if (isChecked) { 2811 // Add to collections array if not already there 2812 if (!page.terms.collections.some(c => c.id === collectionId)) { 2813 page.terms.collections.push({ id: collectionId, label: collectionTitle }); 2814 } 2815 page.addPill(collectionId, collectionTitle, "collection"); 2816 // Check the corresponding checkbox in the other panel 2817 $("#FiltersList #CollectionCheckboxList input[value='" + collectionId + "']").prop("checked", true); 2818 } else { 2819 // Remove from collections array 2820 page.terms.collections = page.terms.collections.filter(c => c.id !== collectionId); 2821 page.pills(); 2822 // Uncheck the corresponding checkbox in the other panel 2823 $("#FiltersList #CollectionCheckboxList input[value='" + collectionId + "']").prop("checked", false); 2824 } 2825 2826 // Set single collection filter (backend expects singular 'collection' parameter) 2827 products.filter.collection = page.terms.collections.length > 0 ? page.terms.collections[0].id : -1;
2828 page.pillsChanged(); 2829 } else if (name === 'gender-filter') { 2830 const genderValue = value; 2831 const genderLabels = { 'M': 'Mens', 'W': 'Womens', 'U': 'Unisex', 'K': 'Kids' }; 2832 const isChecked = $(this).is(':checked'); 2833 2834 if (isChecked) { 2835 if (!page.terms.genders) page.terms.genders = []; 2836 if (!page.terms.genders.includes(genderValue)) { 2837 page.terms.genders.push(genderValue); 2838 page.addPill(genderValue, genderLabels[genderValue], 'gender'); 2839 } 2840 // Sync desktop checkbox 2841 $('#FiltersList #GenderCheckboxList input[value="' + genderValue + '"]').prop('checked', true); 2842 } else { 2843 if (page.terms.genders) { 2844 const idx = page.terms.genders.indexOf(genderValue); 2845 if (idx !== -1) page.terms.genders.splice(idx, 1); 2846 } 2847 page.pills(); 2848 // Sync desktop checkbox 2849 $('#FiltersList #GenderCheckboxList input[value="' + genderValue + '"]').prop('checked', false); 2850 } 2851 2852 products.filter.genders = page.terms.genders || []; 2853 page.pillsChanged(); 2854 } else if (name === 'shipping-options' || name === 'sale-option') { 2855 // Handle shipping and sale checkboxes - these don't create pills 2856 page.pills(); 2857 } 2858 }); 2859 2860 // Re-attach brand search input handler (ID is prefixed with "mobile-") 2861 $("#MobileFilterContent #mobile-BrandSearchInput").off("input").on("input", function() { 2862 page.filterBrandCheckboxes($(this).val()); 2863 }); 2864 2865 $("#MobileFilterContent #mobile-CollectionSearchInput").off("input").on("input", function() { 2866 page.filterCollectionCheckboxes($(this).val()); 2867 }); 2868 2869 $("#MobileFilterContent #mobile-SizeSearchInput").off("input").on("input", function() { 2870 page.filterSizeCheckboxes($(this).val()); 2871 }); 2872 2873 $("#MobileFilterContent #mobile-WidthSearchInput").off("input").on("input", function() { 2874 page.filterWidthCheckboxes($(this).val()); 2875 }); 2876 2877 $("#MobileFilterContent #mobile-CategorySearchInput").off("input").on("input", function() { 2878 page.filterCategoryLinks($(this).val()); 2879 }); 2880 2881 // Re-attach show more handlers 2882 $("#MobileFilterContent #BrandArea #BrandShowMore a").off('click').on('click', function(e) { 2883 e.preventDefault(); 2884 page.showAllBrands(); 2885 }); 2886 $("#MobileFilterContent #CategoryArea #CategoryShowMore a").off('click').on('click', function(e) { 2887 e.preventDefault(); 2888 page.showAllCategories(); 2889 }); 2890 $("#MobileFilterContent #CollectionArea #CollectionShowMore a").off('click').on('click', function(e) { 2891 e.preventDefault(); 2892 page.showAllCollections(); 2893 }); 2894 $("#MobileFilterContent #SizeArea #SizeShowMore a").off('click').on('click', function(e) { 2895 e.preventDefault(); 2896 page.expandAllSizes(); 2897 }); 2898 $("#MobileFilterContent #WidthArea #WidthShowMore a").off('click').on('click', function(e) { 2899 e.preventDefault(); 2900 page.expandAllWidths(); 2901 }); 2902 $("#MobileFilterContent #ColorArea #ColorShowMore a").off('click').on('click', function(e) { 2903 e.preventDefault(); 2904 page.showAllColors(this); 2905 }); 2906 2907 // Re-attach category link handlers 2908 $("#MobileFilterContent .category-toggle").off('click').on('click', page.toggleCategoryChildren); 2909 2910 $("#MobileFilterContent .category-link").off('click').on('click', function(e) { 2911 e.stopPropagation(); 2912 e.preventDefault(); 2913 const categoryId = parseInt($(this).data('category-id')); 2914 const categoryLabel = $(this).text().replace(/\s*\(\d+\)/, '').trim(); 2915 2916 // Remove selected class from all category links 2917 $("#MobileFilterContent .category-link").removeClass("selected"); 2918 $("#FiltersList .category-link").removeClass("selected"); 2919 2920 // Add selected class to clicked link and its desktop counterpart 2921 $(this).addClass("selected"); 2922 $("#FiltersList .category-link[data-category-id='" + categoryId + "']").addClass("selected"); 2923 2924 // Update filter state 2925 page.terms.categories.id = categoryId; 2926 page.terms.categories.label = categoryLabel; 2927 page.addPill(categoryId, categoryLabel, "category"); 2928 2929 products.filter.category = page.terms.categories.id; 2930 2931 return false; 2932 }); 2933 2934 // Re-attach price input handlers for mobile 2935 const updateApplyButtonOpacity = function($container) { 2936 const hasMin = $container.find("input[data-filter='price-min']").val().trim() !== ''; 2937 const hasMax = $container.find("input[data-filter='price-max']").val().trim() !== ''; 2938 $container.find("button[data-filter='apply-price']").css('opacity', (hasMin || hasMax) ? '1' : '0.5'); 2939 }; 2940 2941 $("#MobileFilterContent input[data-filter='price-min'], #MobileFilterContent input[data-filter='price-max']").off('input blur focus').on('input', function() { 2942 const $container = $("#MobileFilterContent"); 2943 updateApplyButtonOpacity($container); 2944 }).on('blur', function() { 2945 let val = $(this).val().replace(/[^0-9.]/g, ''); 2946 if (val) { 2947 $(this).val('$' + val); 2948 } 2949 }).on('focus', function() { 2950 let val = $(this).val().replace('$', ''); 2951 $(this).val(val); 2952 // Select all text so user can replace by typing (Bug #2 fix) 2953 $(this).select(); 2954 }); 2955 2956 $("#MobileFilterContent button[data-filter='apply-price']").off('click').on('click', function() { 2957 const $container = $("#MobileFilterContent"); 2958 const min = parseFloat($container.find('#PriceMin').val().replace('$', '')); 2959 const max = parseFloat($container.find('#PriceMax').val().replace('$', '')); 2960 2961 if (!isNaN(min) && !isNaN(max) && min <= max) { 2962 const customValue = min + '-' + max; 2963 const customLabel = '$' + min + ' - $' + max; 2964 2965 // Uncheck all price checkboxes in both panels 2966 $('#PriceCheckboxList input[type="checkbox"], #MobileFilterContent .filter-content input[type="radio"][name="priceRange"]').prop('checked', false); 2967 2968 // Replace prices array with only this custom range 2969 page.terms.prices = [customValue]; 2970 2971 // Add pill 2972 page.addPill(customValue, customLabel, 'price'); 2973 2974 // Update filter 2975 products.filter.prices = page.terms.prices;
2976 // page.pillsChanged(); 2977 2978 // Clear inputs in both panels 2979 $("input[data-filter='price-min']").val(''); 2980 $("input[data-filter='price-max']").val(''); 2981 updateApplyButtonOpacity($('#FiltersList')); 2982 updateApplyButtonOpacity($('#MobileFilterContent')); 2983 } else { 2984 alert('Please enter valid min and max prices (min must be less than or equal to max)'); 2985 } 2986 }); 2987 } 2988 2989 // Sync checkboxes from desktop to mobile to reflect current filter state 2990 $("#FiltersList input[type='checkbox']").each(function() { 2991 const name = $(this).attr('name'); 2992 const value = $(this).val(); 2993 const isChecked = $(this).is(':checked'); 2994 const $mobileCheckbox = $("#MobileFilterContent input[name='" + name + "'][value='" + value + "']"); 2995 $mobileCheckbox.prop('checked', isChecked); 2996 }); 2997 2998 // Sync pills from desktop to mobile 2999 page.syncMobilePills(); 3000 3001 // Update product count before opening 3002 page.updateMobileProductCount(); 3003 3004 // Expand all filter sections in mobile panel 3005 $('#MobileFilterContent .filter-content').show(); 3006 3007 // Re-attach ALL show more handlers every time (to ensure they work even if data loads late) 3008 $("#MobileFilterContent #BrandArea #BrandShowMore a").off('click').on('click', function(e) { 3009 e.preventDefault(); 3010 page.showAllBrands(); 3011 }); 3012 $("#MobileFilterContent #CategoryArea #CategoryShowMore a").off('click').on('click', function(e) { 3013 e.preventDefault(); 3014 page.showAllCategories(); 3015 }); 3016 $("#MobileFilterContent #CollectionArea #CollectionShowMore a").off('click').on('click', function(e) { 3017 e.preventDefault(); 3018 page.showAllCollections(); 3019 }); 3020 $("#MobileFilterContent #SizeArea #SizeShowMore a").off('click').on('click', function(e) { 3021 e.preventDefault(); 3022 page.expandAllSizes(); 3023 }); 3024 $("#MobileFilterContent #WidthArea #WidthShowMore a").off('click').on('click', function(e) { 3025 e.preventDefault(); 3026 page.expandAllWidths(); 3027 }); 3028 $("#MobileFilterContent #ColorArea #ColorShowMore a").off('click').on('click', function(e) { 3029 e.preventDefault(); 3030 page.showAllColors(this); 3031 }); 3032 3033 // Prevent body scroll - iOS fix 3034 $('body').addClass('mobile-filter-open'); 3035 3036 // Show overlay and panel 3037 $('#MobileFilterOverlay').addClass('active'); 3038 $('#MobileFilterPanel').addClass('active'); 3039 3040 // Fix flexbox layout issue - ensure filter content is positioned correctly 3041 $('.mobile-filter-content').css({ position: 'relative', top: '0' }); 3042 $('.mobile-filter-content').scrollTop(0); 3043 }, 3044 closeMobileFilters: function() { 3045 // Restore body scroll 3046 $('body').removeClass('mobile-filter-open'); 3047 3048 // Hide overlay and panel 3049 $('#MobileFilterOverlay').removeClass('active'); 3050 $('#MobileFilterPanel').removeClass('active'); 3051 }, 3052 applyMobileFilters: function() { 3053 // Trigger filter search 3054 page.pills(); 3055 3056 // Close panel 3057 page.closeMobileFilters(); 3058 }, 3059 syncCheckboxes: function(name, value) { 3060 // Determine if checkbox should be checked based on current filter state 3061 let isChecked = false; 3062 3063 if (name === 'brand-filter') { 3064 isChecked = page.terms.brands.some(b => b.id === parseInt(value)); 3065 } else if (name === 'size-filter') { 3066 isChecked = page.terms.sizes.includes(value); 3067 } else if (name === 'width-filter') { 3068 isChecked = page.terms.widths.includes(value); 3069 } 3070 3071 // Update both desktop and mobile checkboxes to match filter state 3072 const $mobileCheckbox = $(`#MobileFilterContent input[name="${name}"][value="${value}"]`); 3073 const $desktopCheckbox = $(`#FiltersList input[name="${name}"][value="${value}"]`); 3074 $desktopCheckbox.prop('checked', isChecked); 3075 $mobileCheckbox.prop('checked', isChecked); 3076 }, 3077 updateMobileProductCount: function() { 3078 // Use the displayed product count from SearchTotal (handles both filtered and unfiltered states) 3079 const displayedCount = $("#SearchTotal").text().trim(); 3080 const count = displayedCount ? parseInt(displayedCount, 10) : 0; 3081 $("#MobileFilterCount").text(count); 3082 }, 3083 syncMobilePills: function() { 3084 // Clone pills from desktop to mobile 3085 const desktopPills = $("#PillBox").html(); 3086 $("#MobilePillBox").html(desktopPills); 3087 3088 // Show/hide mobile pill box area based on whether there are pills 3089 if (desktopPills && desktopPills.trim().length > 0) { 3090 $("#MobilePillBoxArea").show(); 3091 $("#MobileClearFilterButton").show(); 3092 } else { 3093 $("#MobilePillBoxArea").hide(); 3094 $("#MobileClearFilterButton").hide(); 3095 } 3096 3097 // Re-attach click handlers to mobile pills 3098 $("#MobilePillBox .pill").off('click').on('click', page.removeSearch); 3099 3100 // Attach clear button handler 3101 $("#MobileClearFilterButton").off('click').on('click', function() { 3102 page.clear(); 3103 page.closeMobileFilters(); 3104 }); 3105 }, 3106 allSizes: [], 3107 currentFilteredSizes: null, 3108 sizesShowingAll: false, 3109 sizesLoaded: function(data) { 3110 // Store all sizes for show more functionality 3111 page.allSizes = data; 3112 3113 // Setup show more functionality
3114 $("#SizeShowMore a").off("click").on("click", function(e) { 3115 e.preventDefault(); 3116 page.expandAllSizes(); 3117 }); 3118 3119 page.renderSizeCheckboxes(); 3120 }, 3121 renderSizeCheckboxes: function(sizes, showAll = false) { 3122 sizes = sizes || page.allSizes; 3123 page.sizesShowingAll = showAll; 3124 const sizesToShow = showAll ? sizes : sizes.slice(0, 5); 3125 const remainingCount = sizes.length - sizesToShow.length; 3126 3127 let html = ''; 3128 for (let i = 0; i < sizesToShow.length; i++) { 3129 const sizeData = sizesToShow[i]; 3130 const size = sizeData.size !== undefined ? Number(sizeData.size).toFixed(1).replace('.0', '') : sizeData; 3131 const sizeStr = size.toString(); 3132 const isChecked = page.terms.sizes.includes(sizeStr) ? 'checked' : ''; 3133 3134 html += ` 3135 <div class="input-checkbox"> 3136 <input id="size-${i}" type="checkbox" name="size-filter" value="${sizeStr}" ${isChecked} data-size="${sizeStr}" /> 3137 <label for="size-${i}" class="runfreecheck"></label> 3138 <span class="checkboxlabel">${size}</span> 3139 </div> 3140 `; 3141 } 3142 3143 // Update both desktop and mobile (if mobile panel is open) 3144 $("#FiltersList #SizeCheckboxList").html(html); 3145 $("#MobileFilterContent #SizeCheckboxList").html(html); 3146 3147 // Toggle button text (both desktop and mobile) 3148 if (showAll) { 3149 $("#FiltersList #SizeShowMore a").html('Show less'); 3150 $("#MobileFilterContent #SizeArea #SizeShowMore a").html('Show Less'); 3151 $("#SizeShowMore").show(); 3152 } else if (remainingCount > 0) { 3153 $("#SizeShowMoreCount").text(remainingCount); 3154 $("#FiltersList #SizeShowMore a").html(`Show <span>${remainingCount}</span> more`); 3155 $("#MobileFilterContent #SizeArea #SizeShowMoreCount").text(remainingCount); 3156 $("#MobileFilterContent #SizeArea #SizeShowMore a").html(`Show <span>${remainingCount}</span> More`); 3157 $("#SizeShowMore").show(); 3158 $("#MobileFilterContent #SizeArea #SizeShowMore").show(); 3159 } else { 3160 $("#SizeShowMore").hide(); 3161 $("#MobileFilterContent #SizeArea #SizeShowMore").hide(); 3162 } 3163 3164 // Show size area if we have sizes 3165 if (page.allSizes.length > 0) { 3166 $("#SizeArea").show(); 3167 } 3168 3169 // Attach change handlers to both desktop and mobile checkboxes 3170 $("#FiltersList #SizeCheckboxList input[type='checkbox'], #MobileFilterContent #SizeArea input[type='checkbox']").off("change").on("change", function() { 3171 const size = $(this).val(); 3172 3173 if ($(this).is(":checked")) { 3174 if (!page.terms.sizes.includes(size)) { 3175 page.terms.sizes.push(size); 3176 } 3177 page.addPill(size, size, "size"); 3178 // Check the corresponding checkbox in the other panel 3179 $("#FiltersList #SizeCheckboxList input[value='" + size + "']").prop("checked", true); 3180 $("#MobileFilterContent #SizeCheckboxList input[value='" + size + "']").prop("checked", true); 3181 } else { 3182 const index = page.terms.sizes.indexOf(size); 3183 if (index > -1) { 3184 page.terms.sizes.splice(index, 1); 3185 } 3186 products.filter.sizes = page.terms.sizes; 3187 page.pills(); 3188 // Uncheck the corresponding checkbox in the other panel 3189 $("#FiltersList #SizeCheckboxList input[value='" + size + "']").prop("checked", false); 3190 $("#MobileFilterContent #SizeCheckboxList input[value='" + size + "']").prop("checked", false); 3191 } 3192 3193 products.filter.sizes = page.terms.sizes; 3194 // page.pillsChanged(); 3195 }); 3196 }, 3197 expandAllSizes: function() { 3198 const desktopText = $("#FiltersList #SizeArea #SizeShowMore a").text().trim(); 3199 const mobileText = $("#MobileFilterContent #SizeArea #SizeShowMore a").text().trim(); 3200 const isShowingAll = desktopText === 'Show less' || mobileText === 'Show Less'; 3201 const currentData = page.currentFilteredSizes || page.allSizes;
3202 page.renderSizeCheckboxes(currentData, !isShowingAll); 3203 }, 3204 toggleFilters: function(data, close) { 3205 if ($('#FiltersList').is(":visible") || close) { 3206 $('#FiltersArea').addClass('closed'); 3207 $('#FiltersExpand').addClass('closed'); 3208 $('#FiltersList').hide(); 3209 $('#FiltersArea .select-option .ti-angle-down').hide(); 3210 } else { 3211 $('#FiltersArea').removeClass('closed') 3212 $('#FiltersExpand').removeClass('closed'); 3213 $('#FiltersList').show(); 3214 $('#FiltersArea .select-option .ti-angle-down').show(); 3215 } 3216 }, 3217 toggleSort: function(data, close) { 3218 if ($('#SortBy').is(":visible") || close) { 3219 $('#SortArea').addClass('closed'); 3220 $('#SortExpand').addClass('closed'); 3221 $('#SortBy').hide(); 3222 $('#SortArea .select-option .ti-angle-down').hide(); 3223 } else { 3224 $('#SortArea').removeClass('closed'); 3225 $('#SortExpand').removeClass('closed'); 3226 $('#SortBy').show(); 3227 $('#SortArea .select-option .ti-angle-down').show(); 3228 } 3229 }, 3230 toggleBrands: function(data, close) { 3231 const $container = $(this).closest('#BrandArea'); 3232 const $content = $container.find('.filter-content'); 3233 const $icon = $container.find('.toggleright'); 3234 3235 if ($content.is(":visible") || close) { 3236 $container.addClass('closed'); 3237 $icon.addClass('closed'); 3238 $content.hide(); 3239 } else { 3240 $container.removeClass('closed'); 3241 $icon.removeClass('closed'); 3242 $content.show(); 3243 } 3244 }, 3245 toggleCollections: function(data, close) { 3246 const $container = $(this).closest('#CollectionArea'); 3247 const $content = $container.find('.filter-content'); 3248 const $icon = $container.find('.toggleright'); 3249 3250 if ($content.is(":visible") || close) { 3251 $container.addClass('closed'); 3252 $icon.addClass('closed'); 3253 $content.hide(); 3254 } else { 3255 $container.removeClass('closed'); 3256 $icon.removeClass('closed'); 3257 $content.show(); 3258 } 3259 }, 3260 toggleCategories: function(data, close) { 3261 const $container = $(this).closest('#CategoryArea'); 3262 const $content = $container.find('#CategoryList'); 3263 const $icon = $container.find('.toggleright'); 3264 3265 if ($content.is(":visible") || close) { 3266 $container.addClass('closed'); 3267 $icon.addClass('closed'); 3268 $content.hide(); 3269 } else { 3270 $container.removeClass('closed'); 3271 $icon.removeClass('closed'); 3272 $content.show(); 3273 } 3274 }, 3275 toggleSizes: function(data, close) { 3276 const $container = $(this).closest('#SizeArea'); 3277 const $content = $container.find('.filter-content'); 3278 const $icon = $container.find('.toggleright'); 3279 3280 if ($content.is(":visible") || close) { 3281 $container.addClass('closed'); 3282 $icon.addClass('closed'); 3283 $content.hide(); 3284 } else { 3285 $container.removeClass('closed'); 3286 $icon.removeClass('closed'); 3287 $content.show(); 3288 } 3289 }, 3290 toggleWidths: function(data, close) { 3291 const $container = $(this).closest('#WidthArea'); 3292 const $content = $container.find('.filter-content'); 3293 const $icon = $container.find('.toggleright'); 3294 3295 if ($content.is(":visible") || close) { 3296 $container.addClass('closed'); 3297 $icon.addClass('closed'); 3298 $content.hide(); 3299 } else { 3300 $container.removeClass('closed'); 3301 $icon.removeClass('closed'); 3302 $content.show(); 3303 } 3304 }, 3305 togglePrice: function(data, close) { 3306 const $container = $(this).closest('#PriceArea'); 3307 const $content = $container.find('.filter-content'); 3308 const $icon = $container.find('.toggleright'); 3309 3310 if ($content.is(":visible") || close) { 3311 $container.addClass('closed'); 3312 $icon.addClass('closed'); 3313 $content.hide(); 3314 } else { 3315 $container.removeClass('closed'); 3316 $icon.removeClass('closed'); 3317 $content.show(); 3318 } 3319 }, 3320 toggleColor: function(data, close) { 3321 const $container = $(this).closest('#ColorArea'); 3322 const $content = $container.find('.filter-content'); 3323 const $icon = $container.find('.toggleright'); 3324 3325 if ($content.is(":visible") || close) { 3326 $container.addClass('closed'); 3327 $icon.addClass('closed'); 3328 $content.hide(); 3329 } else { 3330 $container.removeClass('closed'); 3331 $icon.removeClass('closed'); 3332 $content.show(); 3333 } 3334 }, 3335 togglePickup: function(data, close) { 3336 const $container = $(this).closest('#ShippingArea'); 3337 const $content = $container.find('#ShippingList'); 3338 const $icon = $container.find('.toggleright'); 3339 3340 if ($content.is(":visible") || close) { 3341 $container.addClass('closed'); 3342 $icon.addClass('closed'); 3343 $content.hide(); 3344 } else { 3345 $container.removeClass('closed'); 3346 $icon.removeClass('closed'); 3347 $content.show(); 3348 } 3349 }, 3350 toggleReturn: function(data, close) { 3351 const $container = $(this).closest('#ReturnPolicy'); 3352 const $content = $container.find('#miniReturn'); 3353 const $icon = $container.find('.toggleright'); 3354 3355 if ($content.is(":visible") || close) { 3356 $container.addClass('closed'); 3357 $icon.addClass('closed'); 3358 $content.hide(); 3359 } else { 3360 $container.removeClass('closed'); 3361 $icon.removeClass('closed'); 3362 $content.show(); 3363 } 3364 }, 3365 togglePopular: function(data, close) { 3366 const $container = $(this).closest('#PopularArea'); 3367 const $content = $container.find('#PopularList'); 3368 const $icon = $container.find('.toggleright'); 3369 3370 if ($content.is(":visible") || close) { 3371 $container.addClass('closed'); 3372 $icon.addClass('closed'); 3373 $content.hide(); 3374 } else { 3375 $container.removeClass('closed'); 3376 $icon.removeClass('closed'); 3377 $content.show(); 3378 } 3379 }, 3380 allWidths: [], 3381 currentFilteredWidths: null, 3382 widthsShowingAll: false, 3383 widthsLoaded: function(data) { 3384 // Transform data to include sizeAppend as width property if needed
3385 page.allWidths = data.map(item => { 3386 if (typeof item === 'object' && item.sizeAppend !== undefined) { 3387 return { 3388 width: item.sizeAppend, 3389 productCount: item.productCount || 0 3390 }; 3391 } 3392 return { 3393 width: item.toString(), 3394 productCount: 0 3395 }; 3396 }); 3397 3398 // Setup show more functionality 3399 $("#WidthShowMore a").off("click").on("click", function(e) { 3400 e.preventDefault(); 3401 page.expandAllWidths(); 3402 }); 3403 3404 page.renderWidthCheckboxes(); 3405 }, 3406 renderWidthCheckboxes: function(widths, showAll = false) { 3407 widths = widths || page.allWidths; 3408 page.widthsShowingAll = showAll; 3409 const widthsToShow = showAll ? widths : widths.slice(0, 5); 3410 const remainingCount = widths.length - widthsToShow.length; 3411 3412 let html = ''; 3413 for (let i = 0; i < widthsToShow.length; i++) { 3414 const widthData = widthsToShow[i]; 3415 const width = widthData.width; 3416 const isChecked = page.terms.widths.includes(width) ? 'checked' : ''; 3417 3418 html += ` 3419 <div class="input-checkbox"> 3420 <input id="width-${i}" type="checkbox" name="width-filter" value="${width}" ${isChecked} data-width="${width}" /> 3421 <label for="width-${i}" class="runfreecheck"></label> 3422 <span class="checkboxlabel">${width}</span> 3423 </div> 3424 `; 3425 } 3426 3427 // Update both desktop and mobile (if mobile panel is open) 3428 $("#FiltersList #WidthCheckboxList").html(html); 3429 $("#MobileFilterContent #WidthCheckboxList").html(html); 3430 3431 // Toggle button text (both desktop and mobile) 3432 if (showAll) { 3433 $("#FiltersList #WidthShowMore a").html('Show less'); 3434 $("#MobileFilterContent #WidthArea #WidthShowMore a").html('Show Less'); 3435 $("#WidthShowMore").show(); 3436 $("#MobileFilterContent #WidthArea #WidthShowMore").show(); 3437 } else if (remainingCount > 0) { 3438 $("#WidthShowMoreCount").text(remainingCount); 3439 $("#FiltersList #WidthShowMore a").html(`Show <span>${remainingCount}</span> more`); 3440 $("#MobileFilterContent #WidthArea #WidthShowMoreCount").text(remainingCount); 3441 $("#MobileFilterContent #WidthArea #WidthShowMore a").html(`Show <span>${remainingCount}</span> More`); 3442 $("#WidthShowMore").show(); 3443 $("#MobileFilterContent #WidthArea #WidthShowMore").show(); 3444 } else { 3445 $("#WidthShowMore").hide(); 3446 $("#MobileFilterContent #WidthArea #WidthShowMore").hide(); 3447 } 3448 3449 // Show width area if we have widths 3450 if (page.allWidths.length > 0) { 3451 $("#WidthArea").show(); 3452 } 3453 3454 // Attach change handlers to both desktop and mobile checkboxes 3455 $("#FiltersList #WidthCheckboxList input[type='checkbox'], #MobileFilterContent #WidthArea input[type='checkbox']").off("change").on("change", function() { 3456 const width = $(this).val(); 3457 3458 if ($(this).is(":checked")) { 3459 if (!page.terms.widths.includes(width)) { 3460 page.terms.widths.push(width); 3461 } 3462 page.addPill(width, width, "width"); 3463 // Check the corresponding checkbox in the other panel 3464 $("#FiltersList #WidthCheckboxList input[value='" + width + "']").prop("checked", true); 3465 $("#MobileFilterContent #WidthCheckboxList input[value='" + width + "']").prop("checked", true); 3466 } else { 3467 const index = page.terms.widths.indexOf(width); 3468 if (index > -1) { 3469 page.terms.widths.splice(index, 1); 3470 } 3471 products.filter.widths = page.terms.widths; 3472 page.pills(); 3473 // Uncheck the corresponding checkbox in the other panel 3474 $("#FiltersList #WidthCheckboxList input[value='" + width + "']").prop("checked", false); 3475 $("#MobileFilterContent #WidthCheckboxList input[value='" + width + "']").prop("checked", false); 3476 } 3477 3478 products.filter.widths = page.terms.widths;
3479 // page.pillsChanged(); 3480 }); 3481 }, 3482 expandAllWidths: function() { 3483 const desktopText = $("#FiltersList #WidthArea #WidthShowMore a").text().trim(); 3484 const mobileText = $("#MobileFilterContent #WidthArea #WidthShowMore a").text().trim(); 3485 const isShowingAll = desktopText === 'Show less' || mobileText === 'Show Less'; 3486 const currentData = page.currentFilteredWidths || page.allWidths; 3487 page.renderWidthCheckboxes(currentData, !isShowingAll); 3488 }, 3489 initializePriceFilters: function() { 3490 const priceRanges = [ 3491 { label: '$0 - $49.99', min: 0, max: 49.99, value: '0-49.99' }, 3492 { label: '$50 - $99.99', min: 50, max: 99.99, value: '50-99.99' }, 3493 { label: '$100 - $199.99', min: 100, max: 199.99, value: '100-199.99' }, 3494 { label: '$200 - $499.99', min: 200, max: 499.99, value: '200-499.99' }, 3495 { label: '$500 - $999.99', min: 500, max: 999.99, value: '500-999.99' } 3496 ]; 3497 3498 // Store price ranges for later use 3499 page.priceRanges = priceRanges; 3500 3501 // Render price ranges without counts 3502 let html = ''; 3503 for (let i = 0; i < priceRanges.length; i++) { 3504 const range = priceRanges[i]; 3505 const isChecked = (page.terms.prices && page.terms.prices.indexOf(range.value) !== -1) ? 'checked' : ''; 3506 3507 html += ` 3508 <div class="input-checkbox"> 3509 <input type="checkbox" value="${range.value}" ${isChecked} id="price_${i}" /> 3510 <label for="price_${i}" class="runfreecheck"></label> 3511 <span class="checkboxlabel">${range.label}</span> 3512 </div> 3513 `; 3514 } 3515 3516 $('#FiltersList #PriceCheckboxList, #MobileFilterContent #PriceArea .filter-content > div:first').html(html); 3517 3518 // Setup click handlers on labels to toggle checkboxes 3519 $('#FiltersList #PriceCheckboxList .checkboxlabel, #MobileFilterContent #PriceArea .checkboxlabel').on('click', function() { 3520 const checkbox = $(this).siblings('input[type="checkbox"]'); 3521 checkbox.prop('checked', !checkbox.prop('checked')).trigger('change'); 3522 }); 3523 3524 // Setup change handlers for checkboxes (both desktop and mobile) 3525 $('#FiltersList #PriceCheckboxList input[type="checkbox"], #MobileFilterContent #PriceArea input[type="checkbox"]').on('change', function() { 3526 const priceRange = $(this).val(); 3527 if ($(this).is(':checked')) { 3528 // Uncheck all other price checkboxes in both desktop and mobile 3529 $('#FiltersList #PriceCheckboxList input[type="checkbox"], #MobileFilterContent #PriceArea input[type="checkbox"]').not(this).prop('checked', false); 3530 3531 // Check this checkbox in both panels 3532 $('#FiltersList #PriceCheckboxList input[value="' + priceRange + '"], #MobileFilterContent #PriceArea input[value="' + priceRange + '"]').prop('checked', true); 3533 3534 // Clear existing prices and add only this one 3535 page.terms.prices = [priceRange]; 3536 page.addPill(priceRange, priceRange, 'price'); 3537 } else { 3538 // Clear prices array 3539 page.terms.prices = []; 3540 page.pills(); 3541 // Uncheck in both panels 3542 $('#FiltersList #PriceCheckboxList input[value="' + priceRange + '"], #MobileFilterContent #PriceCheckboxList input[value="' + priceRange + '"]').prop('checked', false); 3543 } 3544 3545 products.filter.prices = page.terms.prices; 3546 // page.pillsChanged(); 3547 3548 }); 3549 3550 // Setup custom price range handler 3551 const updateApplyButtonOpacity = function($container) { 3552 const hasMin = $container.find('#PriceMin').val().trim() !== ''; 3553 const hasMax = $container.find('#PriceMax').val().trim() !== ''; 3554 $container.find('#ApplyCustomPrice').css('opacity', (hasMin || hasMax) ? '1' : '0.5'); 3555 }; 3556 3557 // Setup gender checkbox handlers 3558 $('#FiltersList #GenderCheckboxList input[type="checkbox"], #MobileFilterContent #GenderArea input[type="checkbox"]').on('change', function() { 3559 const genderValue = $(this).val(); 3560 const genderLabels = { 3561 'M': 'Mens', 3562 'W': 'Womens', 3563 'U': 'Unisex', 3564 'K': 'Kids' 3565 }; 3566 3567 if ($(this).is(':checked')) { 3568 // Add to genders array 3569 if (!page.terms.genders) page.terms.genders = []; 3570 if (!page.terms.genders.includes(genderValue)) { 3571 page.terms.genders.push(genderValue); 3572 page.addPill(genderValue, genderLabels[genderValue], 'gender'); 3573 } 3574 // Check in both panels 3575 $('#FiltersList #GenderCheckboxList input[value="' + genderValue + '"], #MobileFilterContent #GenderArea input[value="' + genderValue + '"]').prop('checked', true); 3576 } else { 3577 // Remove from genders array 3578 if (page.terms.genders) { 3579 const index = page.terms.genders.indexOf(genderValue); 3580 if (index !== -1) { 3581 page.terms.genders.splice(index, 1); 3582 } 3583 } 3584 // Uncheck in both panels 3585 $('#FiltersList #GenderCheckboxList input[value="' + genderValue + '"], #MobileFilterContent #GenderArea input[value="' + genderValue + '"]').prop('checked', false); 3586 // Remove pill 3587 page.pills(); 3588 } 3589 3590 products.filter.genders = page.terms.genders || [];
3591 page.pillsChanged(); 3592 }); 3593 3594 // Add $ formatting to price inputs 3595 const formatPriceInput = function(input) { 3596 let val = $(input).val().replace(/[^0-9.]/g, ''); 3597 if (val) { 3598 $(input).val('$' + val); 3599 } 3600 }; 3601 3602 // Setup handlers for both desktop and mobile price inputs 3603 $("input[data-filter='price-min'], input[data-filter='price-max']").off('input blur focus').on('input', function() { 3604 const $container = $(this).closest('#FiltersList, #MobileFilterContent'); 3605 updateApplyButtonOpacity($container); 3606 }).on('blur', function() { 3607 formatPriceInput(this); 3608 }).on('focus', function() { 3609 let val = $(this).val().replace('$', ''); 3610 $(this).val(val); 3611 // Select all text so user can replace by typing (Bug #2 fix) 3612 $(this).select(); 3613 }); 3614 3615 $("button[data-filter='apply-price']").off('click').on('click', function() { 3616 const $container = $(this).closest('#FiltersList, #MobileFilterContent'); 3617 const min = parseFloat($container.find("input[data-filter='price-min']").val().replace('$', '')); 3618 const max = parseFloat($container.find("input[data-filter='price-max']").val().replace('$', '')); 3619 3620 if (!isNaN(min) && !isNaN(max) && min <= max) { 3621 const customValue = min + '-' + max; 3622 const customLabel = '$' + min + ' - $' + max; 3623 3624 // Uncheck all price checkboxes in both panels 3625 $('#FiltersList #PriceCheckboxList input[type="checkbox"], #MobileFilterContent #PriceArea input[type="checkbox"]').prop('checked', false); 3626 3627 // Replace prices array with only this custom range 3628 page.terms.prices = [customValue]; 3629 3630 // Add pill 3631 page.addPill(customValue, customLabel, 'price'); 3632 3633 // Update filter 3634 products.filter.prices = page.terms.prices; 3635 // page.pillsChanged(); 3636 3637 // Clear inputs in both panels 3638 $("input[data-filter='price-min']").val(''); 3639 $("input[data-filter='price-max']").val(''); 3640 updateApplyButtonOpacity($('#FiltersList')); 3641 updateApplyButtonOpacity($('#MobileFilterContent')); 3642 } else { 3643 alert('Please enter valid min and max prices (min must be less than or equal to max)'); 3644 } 3645 }); 3646 }, 3647 urlParamsChanged: function () { 3648 const urlParams = new URLSearchParams(window.location.search); 3649 var filters = []; 3650 3651 // Check if URL contains a search parameter 3652 const hasSearchParam = urlParams.has('search') || urlParams.has('q'); 3653 const saleParamValue = (urlParams.get('sale') || '').trim().toLowerCase(); 3654 const hasSaleParam = saleParamValue === '1' || saleParamValue === 'true' || saleParamValue === 'yes'; 3655 3656 // If new search from URL and we haven't already processed it, clear ALL old filters for a clean search 3657 if (hasSearchParam && !page.searchParamProcessed) { 3658 page.terms.filters = []; 3659 page.terms.genders = []; 3660 page.terms.brands = []; 3661 page.terms.collections = []; 3662 page.terms.sizes = []; 3663 page.terms.widths = []; 3664 page.terms.prices = []; 3665 page.terms.colors = []; 3666 3667 // Also clear UI checkboxes and pills to prevent them from being re-added 3668 $("#BrandCheckboxList input[type='checkbox']").prop("checked", false); 3669 $("#MobileFilterContent #BrandCheckboxList input[type='checkbox']").prop("checked", false); 3670 $("#SizeCheckboxList input[type='checkbox']").prop("checked", false); 3671 $("#MobileFilterContent #SizeCheckboxList input[type='checkbox']").prop("checked", false); 3672 $("#WidthCheckboxList input[type='checkbox']").prop("checked", false); 3673 $("#MobileFilterContent #WidthCheckboxList input[type='checkbox']").prop("checked", false); 3674 $("#PriceCheckboxList input[type='checkbox']").prop("checked", false); 3675 $("#MobileFilterContent #PriceCheckboxList input[type='checkbox']").prop("checked", false); 3676 $("#GenderCheckboxList input[type='checkbox']").prop("checked", false); 3677 $("#MobileFilterContent #GenderArea input[type='checkbox']").prop("checked", false); 3678 $("#CollectionCheckboxList input[type='checkbox']").prop("checked", false); 3679 $("#MobileFilterContent #CollectionCheckboxList input[type='checkbox']").prop("checked", false); 3680 $("#ColorCheckboxList input[type='checkbox']").prop("checked", false); 3681 $("#MobileFilterContent #ColorCheckboxList input[type='checkbox']").prop("checked", false); 3682 $("input[data-filter='ship-to-me']").prop("checked", false); 3683 $("input[data-filter='pickup-in-store']").prop("checked", false); 3684 $("input[data-filter='on-sale']").prop("checked", false); 3685 3686 // Clear pill box (pills will be regenerated from the new search) 3687 $("#PillBox").html(""); 3688 } 3689 3690 // Process brand filter 3691 if (urlParams.has('brand')) {
3692 const brandValue = urlParams.get('brand'); 3693 if (brandValue) { 3694 const brandParam = brandValue.trim(); 3695 if (true) { 3696 if (true) { 3697 const brandId = parseInt(brandParam); 3698 if (page.brands.length > 0) { 3699 var details = brand.details(brandId); 3700 if (!page.terms.brands.some(b => b.id === brandId)) { 3701 page.terms.brands.push({ id: brandId, label: details.name }); 3702 } 3703 page.addSearch(details.name, brandId, "brand", false); 3704 } else { 3705 page.doAfterBrandLoad = true; 3706 } 3707 } 3708 } 3709 } 3710 } 3711 3712 // Process category filter 3713 if (urlParams.has('category')) { 3714 const categoryValue = urlParams.get('category'); 3715 if (categoryValue) { 3716 const categoryParam = categoryValue.trim(); 3717 if (true) { 3718 if (true) { 3719 page.terms.categories.id = categoryParam; 3720 if (page.categories.length > 0) { 3721 var details = category.details(page.terms.categories.id); 3722 var label = details.name; 3723 if (details.label && details.label.trim().length>0) label = details.label; 3724 page.addSearch(label, page.terms.categories.id, "category", false); 3725 } else { 3726 page.doAfterCatLoad = true; 3727 } 3728 } 3729 } 3730 } 3731 } 3732 3733 // Process search filter 3734 if (urlParams.has('search') || urlParams.has('q')) { 3735 const rawSearch = urlParams.get('search') || urlParams.get('q'); 3736 if (rawSearch && !page.searchParamProcessed) { 3737 // Only process if not already handled during initial page load 3738 if (true) { 3739 if (true) { 3740 const searchValue = decodeURIComponent(rawSearch.trim()); 3741 3742 // Extract gender from the search query (silently for API filtering) 3743 const genderExtraction = page.extractGenderFromQuery(searchValue); 3744 3745 // Store genders for API call and create pills to show active gender filters 3746 if (genderExtraction.genders && genderExtraction.genders.length > 0) { 3747 if (!page.terms.genders) page.terms.genders = []; 3748 const genderLabels = { 'M': 'Mens', 'W': 'Womens', 'U': 'Unisex', 'K': 'Kids' }; 3749 genderExtraction.genders.forEach(g => { 3750 if (!page.terms.genders.includes(g)) { 3751 page.terms.genders.push(g); 3752 // Create pill for the gender filter 3753 page.addPill(g, genderLabels[g], 'gender', false); 3754 } 3755 }); 3756 } 3757 3758 // Remove gender keywords from search since they're now in the genders filter 3759 // This prevents "Mens Ghost 12" from becoming a literal phrase search 3760 let cleanedSearch = searchValue; 3761 if (genderExtraction.genders && genderExtraction.genders.length > 0) { 3762 // Remove gender keywords like Mens, Womens, Men, Women, etc. 3763 cleanedSearch = searchValue.replace(/\b(men'?s?|women'?s?|womens?|wmns|male|female|ladies|boys?|girls?|kids?|youth|children|child|unisex|mns?)\b/gi, ' '); 3764 cleanedSearch = cleanedSearch.trim().replace(/\s+/g, ' '); // Normalize whitespace 3765 } 3766 3767 // Extract colors from the search query (e.g., "red shoes" â apply red color filter) 3768 const colorExtraction = page.extractColorFromQuery(cleanedSearch); 3769 3770 // Store colors for filtering and create pills to show active color filters 3771 if (colorExtraction.colors && colorExtraction.colors.length > 0) { 3772 if (!page.terms.colors) page.terms.colors = []; 3773 if (!page.terms.filters) page.terms.filters = [];
3774 colorExtraction.colors.forEach(colorName => { 3775 if (!page.terms.colors.some(c => c.name === colorName)) { 3776 const colorDef = page.colorDefinitions?.find(c => c.name === colorName); 3777 page.terms.colors.push({ 3778 name: colorName, 3779 hex: colorDef?.hex || '#888888' 3780 }); 3781 // Add color to filters array so it's actually searched 3782 const colorLower = colorName.toLowerCase(); 3783 if (!page.terms.filters.includes(colorLower)) { 3784 page.terms.filters.push(colorLower); 3785 } 3786 // Check the corresponding color checkbox 3787 $(`#ColorCheckboxList input[value="${colorName}"]`).prop("checked", true); 3788 $(`#MobileFilterContent #ColorCheckboxList input[value="${colorName}"]`).prop("checked", true); 3789 } 3790 }); 3791 // Use the cleaned query (with color removed) for the search filter 3792 cleanedSearch = colorExtraction.cleanedQuery; 3793 } 3794 3795 // Extract brands from the search term before adding to filters 3796 if (cleanedSearch && cleanedSearch.length > 0) { 3797 const brandExtraction = extractBrandsFromQuery(cleanedSearch, page.brands || []); 3798 3799 // If brands were detected, store their names for later mapping 3800 if (brandExtraction.detectedBrands && brandExtraction.detectedBrands.length > 0) { 3801 if (!page.pendingBrandNames) page.pendingBrandNames = []; 3802 brandExtraction.detectedBrands.forEach(brand => { 3803 // If we got an ID (brands were already loaded), add to page.terms.brands 3804 if (brand.id) { 3805 if (!page.terms.brands) page.terms.brands = []; 3806 if (!page.terms.brands.some(b => b.id === brand.id)) { 3807 page.terms.brands.push({ 3808 id: brand.id, 3809 label: brand.name 3810 }); 3811 } 3812 } else { 3813 // Store brand name for later mapping when brands load 3814 if (!page.pendingBrandNames.includes(brand.name)) { 3815 page.pendingBrandNames.push(brand.name); 3816 } 3817 } 3818 }); 3819 } 3820 3821 // Add only the cleaned query (without brand names) to filters 3822 if (!page.terms.filters) page.terms.filters = []; 3823 const finalSearchTerm = brandExtraction.cleanedQuery.trim(); 3824 if (finalSearchTerm.length > 0 && !page.terms.filters.includes(finalSearchTerm)) { 3825 page.terms.filters.push(finalSearchTerm); 3826 } 3827 } 3828 } 3829 } 3830 } 3831 } 3832 3833 // Process sort filter 3834 if (urlParams.has('sort')) { 3835 const rawSort = urlParams.get('sort'); 3836 if (rawSort) { 3837 const sortValue = decodeURIComponent(rawSort.trim()); 3838 const sortParts = sortValue.split(/\s+/); 3839 if (sortParts.length === 2) { 3840 products.filter.sort = sortParts[0]; 3841 products.filter.sortType = sortParts[1]; 3842 $('#SortBy').val(sortValue); 3843 } 3844 } 3845 } 3846 3847 if (hasSaleParam) { 3848 $('input[data-filter="on-sale"]').prop('checked', true); 3849 $('#MobileFilterContent input[data-filter="on-sale"]').prop('checked', true); 3850 } 3851 3852 // page.pills(); 3853 }, 3854 brandsLoaded: function (e) { 3855 page.brands = e; 3856 3857 // Ensure brands array exists in page.terms 3858 if (!page.terms.brands) { 3859 page.terms.brands = []; 3860 // Migrate old brand structure if exists 3861 if (page.terms.brand && page.terms.brand.id !== -1) { 3862 page.terms.brands.push({ 3863 id: page.terms.brand.id, 3864 label: page.terms.brand.label 3865 }); 3866 } 3867 } 3868 3869 // Process any pending brand names that were detected during URL parsing 3870 // before the brands list was loaded 3871 if (page.pendingBrandNames && page.pendingBrandNames.length > 0) {
3872 page.pendingBrandNames.forEach(brandName => { 3873 // Find the brand by name (case-insensitive) 3874 const brand = page.brands.find(b => b.name.toLowerCase() === brandName.toLowerCase()); 3875 if (brand && !page.terms.brands.some(b => b.id === brand.brandId)) { 3876 page.terms.brands.push({ 3877 id: brand.brandId, 3878 label: brand.name 3879 }); 3880 } 3881 }); 3882 // Clear pending brand names 3883 page.pendingBrandNames = []; 3884 } 3885 3886 // Sort brands by product count (descending) 3887 page.brands.sort((a, b) => (b.total || b.productCount || 0) - (a.total || a.productCount || 0)); 3888 3889 // Initialize allBrands temporarily with database counts 3890 // These will be updated with accurate API counts shortly 3891 page.allBrands = page.brands 3892 .map(brand => ({ 3893 brandId: brand.brandId, 3894 name: brand.name, 3895 productCount: brand.total || brand.productCount || 0, 3896 isHidden: brand.isHidden || brand.hidden || false 3897 })) 3898 .filter(brand => brand.productCount > 0 && !brand.isHidden) 3899 .sort((a, b) => b.productCount - a.productCount); 3900 3901 // Immediately update with accurate counts from the products API 3902 page.updateFilterCounts(); 3903 3904 // Handle forced brand selection 3905 if (page.brands.length > 0) { 3906 for (var i = 0; i < page.brands.length; i++) { 3907 if (page.forceBrandId > 0) { 3908 if (page.forceBrandId == page.brands[i].brandId) { 3909 const brandId = page.brands[i].brandId; 3910 const brandName = page.brands[i].name; 3911 if (!page.terms.brands.some(b => b.id === brandId)) { 3912 page.terms.brands.push({ id: brandId, label: brandName }); 3913 } 3914 } 3915 } else { 3916 if (page.forceBrand != "" && (page.brands[i].name.toLowerCase() == page.forceBrand.toLowerCase() || page.brands[i].name.split(' ').join('').toLowerCase() == page.forceBrand.split(' ').join(''))) { 3917 const brandId = page.brands[i].brandId; 3918 const brandName = page.brands[i].name; 3919 page.forceBrandId = brandId; // Set the ID so filters are applied 3920 if (!page.terms.brands.some(b => b.id === brandId)) { 3921 page.terms.brands.push({ id: brandId, label: brandName }); 3922 } 3923 } 3924 } 3925 } 3926 } 3927 3928 // Render brand checkboxes 3929 page.renderBrandCheckboxes(); 3930 3931 // Setup search functionality 3932 $("#BrandSearchInput").off("input").on("input", function() { 3933 page.filterBrandCheckboxes($(this).val()); 3934 }); 3935 3936 $("#SizeSearchInput").off("input").on("input", function() { 3937 page.filterSizeCheckboxes($(this).val()); 3938 }); 3939 3940 $("#WidthSearchInput").off("input").on("input", function() { 3941 page.filterWidthCheckboxes($(this).val()); 3942 }); 3943 3944 // Setup show more functionality 3945 $("#BrandShowMore a").off("click").on("click", function(e) { 3946 e.preventDefault(); 3947 page.showAllBrands(); 3948 }); 3949 3950 if (page.doAfterBrandLoad) { 3951 page.urlParamsChanged(); 3952 // After processing URL params with loaded brands, render pills 3953 page.pills(); 3954 } else { 3955 if (page.forceBrand != "" || page.forceBrandId > 0) { 3956 // Reset search state since we now have the brand ID resolved 3957 page.isSearching = false; 3958 page.pills(); 3959 } 3960 else if (page.forceCategory != "" || page.forceCategoryId > 0) { 3961 // Category is forced but brand is not - let categoriesLoaded handle it 3962 // Don't call pills() here to avoid duplicate searches 3963 } else { 3964 // No forced brand or category - call pills() for default product load 3965 page.pills(); 3966 } 3967 } 3968 }, 3969 collectionsLoaded: function (e) { 3970 page.collections = e; 3971 3972 // Ensure collections array exists in page.terms 3973 if (!page.terms.collections) { 3974 page.terms.collections = []; 3975 } 3976 3977 // Hide Collections filter if there are no collections 3978 if (!page.collections || page.collections.length === 0) { 3979 $("#CollectionArea").hide(); 3980 return; 3981 } 3982 3983 // Show Collections filter if it was hidden 3984 $("#CollectionArea").show(); 3985 3986 // Sort collections alphabetically by title 3987 page.collections.sort((a, b) => (a.title || '').localeCompare(b.title || '')); 3988 3989 // Store all collections with their data (no counts needed) 3990 page.allCollections = page.collections 3991 .map(coll => ({ 3992 collectionId: coll.collectionId, 3993 title: coll.title 3994 })) 3995 .filter(coll => coll.collectionId);
3995 // Just filter out any invalid entries 3996 3997 // Render collection checkboxes 3998 page.renderCollectionCheckboxes(); 3999 4000 // Setup search functionality 4001 $("#CollectionSearchInput").off("input").on("input", function() { 4002 page.filterCollectionCheckboxes($(this).val()); 4003 }); 4004 4005 // Setup show more functionality for desktop 4006 $("#CollectionShowMore a").off("click").on("click", function(e) { 4007 e.preventDefault(); 4008 page.showAllCollections(); 4009 }); 4010 4011 // Setup show more for mobile if it exists (may be cloned later) 4012 if ($("#MobileFilterContent #CollectionArea").length > 0) { 4013 $("#MobileFilterContent #CollectionArea #CollectionShowMore a").off('click').on('click', function(e) { 4014 e.preventDefault(); 4015 page.showAllCollections(); 4016 }); 4017 } 4018 }, 4019 renderBrandCheckboxes: function(brands, showAll) { 4020 brands = brands || page.allBrands; 4021 const showCount = showAll ? brands.length : 5; 4022 const displayBrands = brands.slice(0, showCount); 4023 const remainingCount = brands.length - showCount; 4024 4025 let html = ''; 4026 for (let i = 0; i < displayBrands.length; i++) { 4027 const brand = displayBrands[i]; 4028 const isChecked = page.terms.brands.some(b => b.id === brand.brandId) ? 'checked' : ''; 4029 4030 html += ` 4031 <div class="input-checkbox"> 4032 <input id="brand-${brand.brandId}" type="checkbox" name="brand-filter" value="${brand.brandId}" ${isChecked} data-brand-name="${brand.name}" /> 4033 <label for="brand-${brand.brandId}" class="runfreecheck"></label> 4034 <span class="checkboxlabel">${brand.name} <span class="count" style="display:none">(${brand.productCount})</span></span> 4035 </div> 4036 `; 4037 } 4038 4039 $("#BrandCheckboxList").html(html); 4040 // Also update mobile panel if it exists 4041 $("#MobileFilterContent #BrandCheckboxList").html(html); 4042 4043 // Toggle button text (both desktop and mobile) 4044 if (showAll) { 4045 $("#FiltersList #BrandShowMore a").html('Show less'); 4046 $("#MobileFilterContent #BrandArea #BrandShowMore a").html('Show Less'); 4047 $("#BrandShowMore").show(); 4048 $("#MobileFilterContent #BrandArea #BrandShowMore").show(); 4049 } else if (remainingCount > 0) { 4050 $("#BrandShowMoreCount").text(remainingCount); 4051 $("#FiltersList #BrandShowMore a").html(`Show <span>${remainingCount}</span> more`); 4052 $("#MobileFilterContent #BrandArea #BrandShowMoreCount").text(remainingCount); 4053 $("#MobileFilterContent #BrandArea #BrandShowMore a").html(`Show <span>${remainingCount}</span> More`); 4054 $("#BrandShowMore").show(); 4055 $("#MobileFilterContent #BrandArea #BrandShowMore").show(); 4056 } else { 4057 $("#BrandShowMore").hide(); 4058 $("#MobileFilterContent #BrandArea #BrandShowMore").hide(); 4059 } 4060 4061 // Attach change handlers to both desktop and mobile checkboxes 4062 $("#FiltersList #BrandCheckboxList input[type='checkbox'], #MobileFilterContent #BrandArea input[type='checkbox']").off("change").on("change", function() { 4063 const brandId = parseInt($(this).val()); 4064 const brandName = $(this).data("brand-name"); 4065 4066 if ($(this).is(":checked")) { 4067 // Add to brands array if not already there 4068 if (!page.terms.brands.some(b => b.id === brandId)) { 4069 page.terms.brands.push({ id: brandId, label: brandName }); 4070 } 4071 page.addPill(brandId, brandName, "brand"); 4072 // Check the corresponding checkbox in the other panel 4073 $("#FiltersList #BrandCheckboxList input[value='" + brandId + "']").prop("checked", true); 4074 $("#MobileFilterContent #BrandArea input[value='" + brandId + "']").prop("checked", true); 4075 } else { 4076 // Remove from brands array 4077 page.terms.brands = page.terms.brands.filter(b => b.id !== brandId); 4078 page.pills(); 4079 // Uncheck the corresponding checkbox in the other panel 4080 $("#FiltersList #BrandCheckboxList input[value='" + brandId + "']").prop("checked", false); 4081 $("#MobileFilterContent #BrandArea input[value='" + brandId + "']").prop("checked", false); 4082 } 4083 4084 products.filter.brands = page.terms.brands.map(b => b.id);
4085 page.pillsChanged(); 4086 }); 4087 }, 4088 filterBrandCheckboxes: function(searchTerm) { 4089 if (!searchTerm || searchTerm.trim() === '') { 4090 page.renderBrandCheckboxes(); 4091 return; 4092 } 4093 4094 const filtered = page.allBrands.filter(brand => 4095 brand.name.toLowerCase().includes(searchTerm.toLowerCase()) 4096 ); 4097 4098 page.renderBrandCheckboxes(filtered, true); 4099 }, 4100 filterSizeCheckboxes: function(searchTerm) { 4101 if (!searchTerm || searchTerm.trim() === '') { 4102 page.currentFilteredSizes = null; 4103 page.renderSizeCheckboxes(); 4104 return; 4105 } 4106 4107 const filtered = page.allSizes.filter(sizeData => { 4108 const size = sizeData.size !== undefined ? Number(sizeData.size).toFixed(1).replace('.0', '') : sizeData; 4109 return size.toString().toLowerCase().includes(searchTerm.toLowerCase()); 4110 }); 4111 4112 page.currentFilteredSizes = filtered; 4113 page.renderSizeCheckboxes(filtered, true); 4114 }, 4115 filterWidthCheckboxes: function(searchTerm) { 4116 if (!searchTerm || searchTerm.trim() === '') { 4117 page.currentFilteredWidths = null; 4118 page.renderWidthCheckboxes(); 4119 return; 4120 } 4121 4122 const filtered = page.allWidths.filter(widthData => 4123 widthData.width.toLowerCase().includes(searchTerm.toLowerCase()) 4124 ); 4125 4126 page.currentFilteredWidths = filtered; 4127 page.renderWidthCheckboxes(filtered, true); 4128 }, 4129 showAllBrands: function() { 4130 const desktopText = $("#FiltersList #BrandShowMore a").text().trim(); 4131 const mobileText = $("#MobileFilterContent #BrandShowMore a").text().trim(); 4132 const isShowingAll = desktopText === 'Show less' || mobileText === 'Show Less'; 4133 page.renderBrandCheckboxes(page.allBrands, !isShowingAll); 4134 }, 4135 renderCollectionCheckboxes: function(collections, showAll) { 4136 collections = collections || page.allCollections; 4137 const showCount = showAll ? collections.length : 5; 4138 const displayCollections = collections.slice(0, showCount); 4139 const remainingCount = collections.length - showCount; 4140 4141 let html = ''; 4142 for (let i = 0; i < displayCollections.length; i++) { 4143 const collection = displayCollections[i]; 4144 const isChecked = page.terms.collections.some(c => c.id === collection.collectionId) ? 'checked' : ''; 4145 4146 html += ` 4147 <div class="input-checkbox"> 4148 <input id="collection-${collection.collectionId}" type="checkbox" name="collection-filter" value="${collection.collectionId}" ${isChecked} data-collection-title="${collection.title}" /> 4149 <label for="collection-${collection.collectionId}" class="runfreecheck"></label> 4150 <span class="checkboxlabel">${collection.title}</span> 4151 </div> 4152 `; 4153 } 4154 4155 $("#CollectionCheckboxList").html(html); 4156 // Also update mobile panel if it exists 4157 $("#MobileFilterContent #CollectionCheckboxList").html(html); 4158 4159 // Toggle button text (both desktop and mobile) 4160 if (showAll) { 4161 $("#FiltersList #CollectionShowMore a").html('Show less'); 4162 $("#MobileFilterContent #CollectionArea #CollectionShowMore a").html('Show Less'); 4163 $("#CollectionShowMore").show(); 4164 $("#MobileFilterContent #CollectionArea #CollectionShowMore").show(); 4165 } else if (remainingCount > 0) { 4166 $("#CollectionShowMoreCount").text(remainingCount); 4167 $("#FiltersList #CollectionShowMore a").html(`Show <span>${remainingCount}</span> more`); 4168 $("#MobileFilterContent #CollectionArea #CollectionShowMoreCount").text(remainingCount); 4169 $("#MobileFilterContent #CollectionArea #CollectionShowMore a").html(`Show <span>${remainingCount}</span> More`); 4170 $("#CollectionShowMore").show(); 4171 $("#MobileFilterContent #CollectionArea #CollectionShowMore").show(); 4172 } else { 4173 $("#CollectionShowMore").hide(); 4174 $("#MobileFilterContent #CollectionArea #CollectionShowMore").hide(); 4175 } 4176 4177 // Attach change handlers to both desktop and mobile checkboxes 4178 $("#FiltersList #CollectionCheckboxList input[type='checkbox'], #MobileFilterContent #CollectionArea input[type='checkbox']").off("
4178change").on("change", function() { 4179 const collectionId = parseInt($(this).val()); 4180 const collectionTitle = $(this).data("collection-title"); 4181 4182 if ($(this).is(":checked")) { 4183 // Add to collections array if not already there 4184 if (!page.terms.collections.some(c => c.id === collectionId)) { 4185 page.terms.collections.push({ id: collectionId, label: collectionTitle }); 4186 } 4187 page.addPill(collectionId, collectionTitle, "collection"); 4188 // Check the corresponding checkbox in the other panel 4189 $("#FiltersList #CollectionCheckboxList input[value='" + collectionId + "']").prop("checked", true); 4190 $("#MobileFilterContent #CollectionArea input[value='" + collectionId + "']").prop("checked", true); 4191 } else { 4192 // Remove from collections array 4193 page.terms.collections = page.terms.collections.filter(c => c.id !== collectionId); 4194 page.pills(); 4195 // Uncheck the corresponding checkbox in the other panel 4196 $("#FiltersList #CollectionCheckboxList input[value='" + collectionId + "']").prop("checked", false); 4197 $("#MobileFilterContent #CollectionArea input[value='" + collectionId + "']").prop("checked", false); 4198 } 4199 4200 // Set single collection filter (backend expects singular 'collection' parameter) 4201 products.filter.collection = page.terms.collections.length > 0 ? page.terms.collections[0].id : -1; 4202 4203 // page.pillsChanged(); 4204 }); 4205 }, 4206 filterCollectionCheckboxes: function(searchTerm) { 4207 if (!searchTerm || searchTerm.trim() === '') { 4208 page.renderCollectionCheckboxes(); 4209 return; 4210 } 4211 4212 const filtered = page.allCollections.filter(collection => 4213 collection.title.toLowerCase().includes(searchTerm.toLowerCase()) 4214 ); 4215 4216 page.renderCollectionCheckboxes(filtered, true); 4217 }, 4218 showAllCollections: function() { 4219 const desktopText = $("#FiltersList #CollectionArea #CollectionShowMore a").text().trim(); 4220 const mobileText = $("#MobileFilterContent #CollectionArea #CollectionShowMore a").text().trim(); 4221 const isShowingAll = desktopText === 'Show less' || mobileText === 'Show Less'; 4222 page.renderCollectionCheckboxes(page.allCollections, !isShowingAll); 4223 }, 4224 updateFilterCounts: function() { 4225 // Combined function to update both brand counts and option counts with a single API call 4226 // Check if there are any non-brand filters applied 4227 const hasNonBrandFilters = ( 4228 (products.filter.sizes && products.filter.sizes.length > 0) || 4229 (products.filter.widths && products.filter.widths.length > 0) || 4230 (products.filter.prices && products.filter.prices.length > 0) || 4231 (page.terms.filters && page.terms.filters.length > 0) || // Color/NLP filters 4232 products.filter.instore || 4233 products.filter.onsale || 4234 products.filter.shipable || 4235 (products.filter.category && products.filter.category !== -1) || 4236 (products.filter.collection && products.filter.collection !== -1) || 4237 (products.filter.search && products.filter.search !== "") 4238 ); 4239 4240 // If ONLY brand filters are applied (no other filters), use the original brand totals 4241 // from page.brands which contains accurate unstacked variant counts from the backend 4242 if (!hasNonBrandFilters) { 4243 page.allBrands = page.brands 4244 .map(brand => ({ 4245 brandId: brand.brandId, 4246 name: brand.name, 4247 productCount: brand.total || brand.productCount || 0, 4248 isHidden: brand.isHidden || brand.hidden || false 4249 })) 4250 .filter(brand => brand.productCount > 0 && !brand.isHidden) 4251 .sort((a, b) => b.productCount - a.productCount); 4252 page.renderBrandCheckboxes(); 4253 4254 // Still need to update option counts even if only brands are filtered 4255 page.updateOptionCountsOnly(); 4256 return; 4257 } 4258 4259 // Make ONE API call that removes both brand and option filters 4260 // Then use client-side filtering to compute both brand and option counts 4261 const countingFilter = JSON.parse(JSON.stringify(products.filter)); 4262 countingFilter.size = 10000; // Get all products 4263 countingFilter.page = 1;
4264 countingFilter.grouped = true; // Use grouped products to match display count 4265 4266 // Remove both brand filters AND option filters to get the superset 4267 delete countingFilter.brands; 4268 delete countingFilter.instore; 4269 delete countingFilter.onsale; 4270 delete countingFilter.shipable; 4271 delete countingFilter.prices; 4272 4273 // NOTE: The backend stored procedure (up_SearchProducts_UI) should handle filter logic as follows: 4274 // - Different filter TYPES are ANDed together: (gender filter) AND (search terms) AND (price filter) AND (size filter) 4275 // - Multiple values within SAME filter type are ORed: (price>=50 AND price<=99.99) OR (price>=100 AND price<=159.99) 4276 // - Search terms within the same query are ANDed: 'Mens Ghost 12' means (Mens) AND (Ghost) AND (12) 4277 // This ensures precise filtering while allowing multiple options within each category 4278 4279 service.call("products-search", countingFilter, function(e) { 4280 if (!e.data || e.data.length === 0) { 4281 // Set all counts to 0 if no products 4282 page.allBrands = []; 4283 page.renderBrandCheckboxes(); 4284 $("#ShipToMe").parent().find('.checkboxlabel').html('Ship To Me <span class="count">(0)</span>'); 4285 $("#PickupInStore").parent().find('.checkboxlabel').html('Pickup In-Store <span class="count">(0)</span>'); 4286 $("#OnSale").parent().find('.checkboxlabel').html('Sale Products <span class="count">(0)</span>'); 4287 page.updatePriceRangeCounts([]); 4288 return; 4289 } 4290 4291 // Apply client-side color filtering if colors are selected 4292 let allProducts = e.data; 4293 if (page.selectedColorFilters && page.selectedColorFilters.length > 0) { 4294 allProducts = e.data.filter(product => 4295 page.productMatchesColorFilter(product.color, page.selectedColorFilters) 4296 ); 4297 } 4298 4299 // ===== BRAND COUNTS ===== 4300 // Filter products by active option filters (instore, onsale, shipable, prices) 4301 let productsForBrandCounts = allProducts; 4302 4303 // Apply option filters client-side 4304 if (products.filter.instore) { 4305 productsForBrandCounts = productsForBrandCounts.filter(p => p.stock > 0); 4306 } 4307 if (products.filter.onsale) { 4308 productsForBrandCounts = productsForBrandCounts.filter(p => 4309 p.isOnSale === true || p.isOnSale === 1 || p.onsale === true || p.onsale === 1 4310 ); 4311 } 4312 if (products.filter.shipable) { 4313 productsForBrandCounts = productsForBrandCounts.filter(p => 4314 !p.isInStorePickupOnly && !p.inStorePickupOnly && 4315 !p.isInStorePurchaseOnly && !p.inStorePurchaseOnly 4316 ); 4317 } 4318 if (page.terms.prices && page.terms.prices.length > 0) { 4319 productsForBrandCounts = productsForBrandCounts.filter(p => { 4320 const price = parseFloat(p.price || p.cost || 0); 4321 return page.terms.prices.some(priceRange => { 4322 const range = page.priceRanges.find(r => r.value === priceRange); 4323 return range && price >= range.min && price <= range.max; 4324 }); 4325 }); 4326 } 4327 4328 // Count unique products per brand 4329 const brandCounts = {}; 4330 const seenBrandProducts = new Set(); 4331 productsForBrandCounts.forEach(product => { 4332 const productKey = `${product.name}|${product.color}`; 4333 if (!seenBrandProducts.has(productKey)) { 4334 seenBrandProducts.add(productKey); 4335 const brandId = product.brandId; 4336 if (brandId) { 4337 brandCounts[brandId] = (brandCounts[brandId] || 0) + 1; 4338 } 4339 } 4340 }); 4341 4342 // Update allBrands with new counts 4343 page.allBrands = page.brands 4344 .map(brand => ({ 4345 brandId: brand.brandId, 4346 name: brand.name, 4347 productCount: brandCounts[brand.brandId] || 0, 4348 isHidden: brand.isHidden || brand.hidden || false 4349 })) 4350 .filter(brand =>
4350 brand.productCount > 0 && !brand.isHidden) 4351 .sort((a, b) => b.productCount - a.productCount); 4352 4353 page.renderBrandCheckboxes(); 4354 4355 // ===== OPTION COUNTS ===== 4356 // Filter products by active brand filters 4357 let productsForOptionCounts = allProducts; 4358 4359 if (products.filter.brands && products.filter.brands.length > 0) { 4360 productsForOptionCounts = productsForOptionCounts.filter(p => 4361 products.filter.brands.includes(p.brandId) 4362 ); 4363 } 4364 4365 // Count unique products for each option 4366 const seenOptionProducts = new Set(); 4367 let shipableCount = 0; 4368 let instoreCount = 0; 4369 let onsaleCount = 0; 4370 4371 productsForOptionCounts.forEach(product => { 4372 const productKey = `${product.name}|${product.color}`; 4373 if (seenOptionProducts.has(productKey)) return; 4374 seenOptionProducts.add(productKey); 4375 4376 if (!product.isInStorePickupOnly && !product.inStorePickupOnly && 4377 !product.isInStorePurchaseOnly && !product.inStorePurchaseOnly) { 4378 shipableCount++; 4379 } 4380 4381 if (product.stock > 0) { 4382 instoreCount++; 4383 } 4384 4385 if (product.isOnSale === true || product.isOnSale === 1 || product.onsale === true || product.onsale === 1) { 4386 onsaleCount++; 4387 } 4388 }); 4389 4390 // Update option labels 4391 $("#ShipToMe").parent().find('.checkboxlabel').html(`Ship To Me <span class="count">(${shipableCount})</span>`); 4392 $("#PickupInStore").parent().find('.checkboxlabel').html(`Pickup In-Store <span class="count">(${instoreCount})</span>`); 4393 $("#OnSale").parent().find('.checkboxlabel').html(`Sale Products <span class="count">(${onsaleCount})</span>`); 4394 4395 // Update price range counts 4396 page.updatePriceRangeCounts(productsForOptionCounts); 4397 }); 4398 }, 4399 updateOptionCountsOnly: function() { 4400 // Separate function for when we only need to update option counts (not brand counts) 4401 // This is called when only brand filters are active 4402 const countingFilter = JSON.parse(JSON.stringify(products.filter)); 4403 countingFilter.size = 10000; 4404 countingFilter.page = 1; 4405 countingFilter.grouped = true; 4406 delete countingFilter.instore; 4407 delete countingFilter.onsale; 4408 delete countingFilter.shipable; 4409 delete countingFilter.prices; 4410 4411 service.call("products-search", countingFilter, function(e) { 4412 if (!e.data || e.data.length === 0) { 4413 $("#ShipToMe").parent().find('.checkboxlabel').html('Ship To Me <span class="count">(0)</span>'); 4414 $("#PickupInStore").parent().find('.checkboxlabel').html('Pickup In-Store <span class="count">(0)</span>'); 4415 $("#OnSale").parent().find('.checkboxlabel').html('Sale Products <span class="count">(0)</span>'); 4416 page.updatePriceRangeCounts([]); 4417 return; 4418 } 4419 4420 let productsForCounting = e.data; 4421 if (page.selectedColorFilters && page.selectedColorFilters.length > 0) { 4422 productsForCounting = e.data.filter(product => 4423 page.productMatchesColorFilter(product.color, page.selectedColorFilters) 4424 ); 4425 } 4426 4427 const seenProducts = new Set(); 4428 let shipableCount = 0; 4429 let instoreCount = 0; 4430 let onsaleCount = 0; 4431 4432 productsForCounting.forEach(product => { 4433 const productKey = `${product.name}|${product.color}`; 4434 if (seenProducts.has(productKey)) return; 4435 seenProducts.add(productKey); 4436 4437 if (!product.isInStorePickupOnly && !product.inStorePickupOnly && 4438 !product.isInStorePurchaseOnly && !product.inStorePurchaseOnly) { 4439 shipableCount++; 4440 } 4441 if (product.stock > 0) { 4442 instoreCount++; 4443 } 4444 if (product.isOnSale === true || product.isOnSale === 1 || product.onsale === true || product.onsale === 1) { 4445 onsaleCount++; 4446 } 4447 }); 4448 4449 $("#ShipToMe").parent().find('.checkboxlabel').html(`Ship To Me <span class="count">(${shipableCount})</span>`); 4450 $("#PickupInStore").parent().find('.checkboxlabel').html(`Pickup In-Store <span class="count">(${instoreCount})</span>`); 4451 $("#OnSale").parent().find('.checkboxlabel').html(`Sale Products <span class="count">(${onsaleCount})</span>`); 4452 4453 page.updatePriceRangeCounts(productsForCounting); 4454 }); 4455 }, 4456 updatePriceRangeCounts: function(products) { 4457 // If no price ranges defined yet, skip 4458 if (!page.priceRanges) { 4459 return; 4460 } 4461 4462 // Count products in each price range 4463 const rangeCounts = page.priceRanges.map(range => { 4464 let count = 0;
4465 products.forEach(product => { 4466 const price = parseFloat(product.price || product.cost || 0); 4467 if (price >= range.min && price <= range.max) { 4468 count++; 4469 } 4470 }); 4471 return { value: range.value, label: range.label, count: count }; 4472 }); 4473 4474 // Hide/show price ranges based on whether products exist in that range (no counts displayed) 4475 rangeCounts.forEach(rangeCount => { 4476 const $desktopContainer = $(`#FiltersList #PriceCheckboxList input[value="${rangeCount.value}"]`).parent(); 4477 const $mobileContainer = $(`#MobileFilterContent #PriceArea input[value="${rangeCount.value}"]`).parent(); 4478 4479 // Hide/show based on count (unless it's currently checked) 4480 const isChecked = $desktopContainer.find('input').prop('checked'); 4481 if (rangeCount.count === 0 && !isChecked) { 4482 $desktopContainer.hide(); 4483 $mobileContainer.hide(); 4484 } else { 4485 $desktopContainer.show(); 4486 $mobileContainer.show(); 4487 } 4488 }); 4489 }, 4490 // Color filter functions 4491 allColors: [], 4492 /** 4493 * Check if a product's color matches any of the selected color filters 4494 * @param {string} productColor - The product's color field (e.g., "Red/Black", "Navy Blue") 4495 * @param {string[]} selectedColors - Array of selected color names (e.g., ["Red", "Blue"]) 4496 * @returns {boolean} - True if product matches any selected color 4497 */ 4498 productMatchesColorFilter: function(productColor, selectedColors) { 4499 if (!selectedColors || selectedColors.length === 0) return true; // No filter = all match 4500 if (!productColor) return false; // No color = doesn't match any filter 4501 4502 const productColorLower = productColor.toLowerCase(); 4503 4504 // Color keyword patterns for flexible matching (same as extractColorFromQuery) 4505 const colorPatterns = { 4506 'red': /\b(red|crimson|scarlet|maroon|burgundy)\b/i, 4507 'orange': /\b(orange|tangerine|coral)\b/i, 4508 'yellow': /\b(yellow|gold|golden|mustard|lemon)\b/i, 4509 'green': /\b(green|olive|lime|mint|teal|emerald|forest)\b/i, 4510 'blue': /\b(blue|navy|aqua|cyan|cobalt|royal|indigo|turquoise)\b/i, 4511 'purple': /\b(purple|violet|lavender|plum|magenta)\b/i, 4512 'pink': /\b(pink|rose|salmon|fuchsia|blush)\b/i, 4513 'beige': /\b(beige|tan|khaki|cream|nude|sand)\b/i, 4514 'brown': /\b(brown|chocolate|coffee|mocha|caramel|chestnut)\b/i, 4515 'black': /\b(black|onyx|charcoal|ebony)\b/i, 4516 'gray': /\b(gray|grey|silver|slate|graphite|ash)\b/i, 4517 'white': /\b(white|ivory|pearl|snow)\b/i 4518 }; 4519 4520 // Check if product matches ANY of the selected colors 4521 for (const color of selectedColors) { 4522 const colorLower = color.toLowerCase(); 4523 const pattern = colorPatterns[colorLower]; 4524 if (pattern && pattern.test(productColorLower)) { 4525 return true; 4526 } 4527 // Fallback: simple substring match 4528 if (productColorLower.includes(colorLower)) { 4529 return true; 4530 } 4531 } 4532 return false; 4533 }, 4534 initColors: function() { 4535 // Initialize colors with hex values (from Backcountry.com) 4536 const colorDefinitions = [ 4537 { name: 'Red', hex: '#DC143C' }, 4538 { name: 'Orange', hex: '#FF8C00' }, 4539 { name: 'Yellow', hex: '#FFD700' }, 4540 { name: 'Green', hex: '#228B22' }, 4541 { name: 'Blue', hex: '#4169E1' }, 4542 { name: 'Purple', hex: '#800080' }, 4543 { name: 'Pink', hex: '#FF69B4' }, 4544 { name: 'Beige', hex: '#F5DEB3' }, 4545 { name: 'Brown', hex: '#8B4513' }, 4546 { name: 'Black', hex: '#000000' }, 4547 { name: 'Gray', hex: '#808080' }, 4548 { name: 'White', hex: '#FFFFFF' } 4549 ]; 4550 4551 // Ensure colors array exists in page.terms 4552 if (!page.terms.colors) { 4553 page.terms.colors = []; 4554 } 4555 4556 // Store color definitions 4557 page.colorDefinitions = colorDefinitions; 4558 4559 // For now, initialize with mock counts (will be replaced with real data later) 4560 page.allColors = colorDefinitions.map(color => ({
4561 name: color.name, 4562 hex: color.hex 4563 })); 4564 4565 page.renderColorCheckboxes(); 4566 }, 4567 renderColorCheckboxes: function(colors, showAll) { 4568 colors = colors || page.allColors; 4569 const showCount = showAll ? colors.length : 6; 4570 const displayColors = colors.slice(0, showCount); 4571 const remainingCount = colors.length - showCount; 4572 4573 let html = ''; 4574 for (let i = 0; i < displayColors.length; i++) { 4575 const color = displayColors[i]; 4576 const isChecked = page.terms.colors.some(c => c.name === color.name) ? 'checked' : ''; 4577 const borderColor = color.name === 'White' ? '#e0e0e0' : color.hex; 4578 4579 html += ` 4580 <div class="input-checkbox"> 4581 <input id="color-${color.name}" type="checkbox" name="color-filter" value="${color.name}" ${isChecked} data-color-hex="${color.hex}" /> 4582 <label for="color-${color.name}" class="runfreecheck"></label> 4583 <span class="color-circle" style="background-color: ${color.hex}; border-color: ${borderColor};"></span> 4584 <span class="checkboxlabel">${color.name}</span> 4585 </div> 4586 `; 4587 } 4588 4589 $("#ColorCheckboxList").html(html); 4590 $("#MobileFilterContent #ColorCheckboxList").html(html); 4591 4592 // Attach change handlers to DESKTOP checkboxes only (mobile handled by general mobile handler) 4593 $("#FiltersList #ColorCheckboxList input[type='checkbox']").off("change").on("change", function() { 4594 const colorName = $(this).val(); 4595 if (!colorName) return; // Guard against null/undefined values 4596 4597 const isChecked = $(this).is(":checked"); 4598 const colorLabel = $(this).siblings('.checkboxlabel').text().trim(); 4599 4600 if (isChecked) { 4601 // Trigger NLP-based AI search for the color using lowercase (this handles pill creation) 4602 page.addSearch(colorName.toLowerCase(), null, 'filter'); 4603 } else { 4604 // Remove the filter when unchecked 4605 page.terms.filters = page.terms.filters.filter(f => f.toLowerCase() !== colorName.toLowerCase()); 4606 page.pills(); 4607 // page.pillsChanged(); 4608 } 4609 4610 // Sync checkbox state between desktop and mobile 4611 $("#FiltersList #ColorCheckboxList input[value='" + colorName + "'], #MobileFilterContent #ColorCheckboxList input[value='" + colorName + "']").prop("checked", isChecked); 4612 }); 4613 4614 // Toggle button text 4615 if (showAll) { 4616 $("#FiltersList #ColorShowMore a").html('Show less'); 4617 $("#MobileFilterContent #ColorArea #ColorShowMore a").html('Show Less'); 4618 $("#ColorShowMore").show(); 4619 $("#MobileFilterContent #ColorArea #ColorShowMore").show(); 4620 } else if (remainingCount > 0) { 4621 $("#ColorShowMoreCount").text(remainingCount); 4622 $("#FiltersList #ColorShowMore a").html(`Show <span>${remainingCount}</span> more`); 4623 $("#MobileFilterContent #ColorArea #ColorShowMoreCount").text(remainingCount); 4624 $("#MobileFilterContent #ColorArea #ColorShowMore a").html(`Show <span>${remainingCount}</span> More`); 4625 $("#ColorShowMore").show(); 4626 $("#MobileFilterContent #ColorArea #ColorShowMore").show(); 4627 } else { 4628 $("#ColorShowMore").hide(); 4629 $("#MobileFilterContent #ColorArea #ColorShowMore").hide(); 4630 } 4631 4632 // Re-attach show more click handler after rendering 4633 $("#ColorShowMore a, #MobileFilterContent #ColorArea #ColorShowMore a").off("click").on("click", function(e) { 4634 e.preventDefault(); 4635 page.showAllColors(this); 4636 }); 4637 }, 4638 showAllColors: function(clickedElement) { 4639 // Check if we're currently showing all colors by looking at any show more link text 4640 const linkText = $(clickedElement || "#ColorShowMore a").text().trim(); 4641 const isShowingAll = linkText.toLowerCase().includes('less'); 4642 page.renderColorCheckboxes(page.allColors, !isShowingAll); 4643 }, 4644 addPill: function (id, name, type, doSearch = true) { 4645 page.addSearch(name, id, type, doSearch); 4646 }, 4647 pillCategory: function (e) { 4648 var id = $(e.currentTarget).data("id"); 4649 var details = category.details(id); 4650 var label = details.name; 4651 if (details.label && details.label.trim().length>0) label = details.label.trim(); 4652 page.addPill(id, label, "category"); 4653 }, 4654 pillBrand: function (e) { 4655 var id = $(e.currentTarget).val(); 4656 var details = brand.details(id); 4657 if (id > 0) { 4658 page.addPill(id, details.name, "brand"); 4659 } 4660 $("#BrandList").val("-1"); 4661 }, 4662 4663 sort: function (e) { 4664 var sortBy = $("#SortBy").val().split(" "); 4665 if (sortBy.length == 2) { 4666 products.filter.page = 1; 4667 if (page.terms.page) page.terms.page = 1; 4668 products.filter.sort = sortBy[0]; 4669 products.filter.sortType = sortBy[1]; 4670 }
4671 page.pillsChanged(true); 4672 }, 4673 lastPillCount: 0, 4674 lastPillSignature: "", 4675 searchDebounceTimer: null, 4676 pillsChanged: (force = false) => { 4677 var pillSignature = $("#PillBox .pill").map(function() { return $(this).data("id") + ":" + $(this).data("type"); }).get().join("|"); 4678 console.log("Pills changed, preparing to execute search... "+ $("#PillBox .pill").length+" pills active. "+force+" force flag."); 4679 4680 if (force || page.lastPillCount!=$("#PillBox .pill").length || page.lastPillSignature!=pillSignature || $("#PillBox .pill").length==0) { 4681 console.log("Pill count changed from "+page.lastPillCount+" to "+$("#PillBox .pill").length); 4682 page.lastPillCount = $("#PillBox .pill").length; 4683 page.lastPillSignature = pillSignature; 4684 4685 if (page.searchDebounceTimer) clearTimeout(page.searchDebounceTimer); 4686 4687 page.searchDebounceTimer = setTimeout(() => { 4688 page.executeSearch(); 4689 }, 100); 4690 } else { 4691 console.log("Pill count unchanged at "+page.lastPillCount); 4692 } 4693 }, 4694 executeSearch: function() { 4695 // debugger; 4696 $(".paging").hide(); 4697 $(".pages").html(""); 4698 $("#BrandingImage").hide(); 4699 $("#ProductList").html($("#CardLoader").html()); 4700 $("#SearchFilterArea").addClass("disabled"); 4701 const urlParams = new URLSearchParams(window.location.search); 4702 if (urlParams.has('c')) { 4703 page.forceCategoryId = -1; 4704 page.forceCategory = ''; 4705 page.forceBrandId = -1; 4706 page.forceBrand = ''; 4707 } 4708 4709 products.filter.brand = -1; 4710 products.filter.category = -1; 4711 delete products.filter.search; // Remove old search field - use searchVariations instead 4712 delete products.filter.searchVariations; // Will be set later if needed 4713 products.filter.instore = $("input[data-filter='pickup-in-store']").first().is(":checked") || null; 4714 products.filter.onsale = $("input[data-filter='on-sale']").first().is(":checked") || null; 4715 products.filter.shipable = $("input[data-filter='ship-to-me']").first().is(":checked") || null; 4716 4717 if (!products.filter.sizes) products.filter.sizes = []; 4718 if (!products.filter.widths) products.filter.widths = []; 4719 if (!products.filter.prices) products.filter.prices = []; 4720 if (!products.filter.brands) products.filter.brands = []; 4721 if (page.forceCategoryId) products.filter.category = page.forceCategoryId; 4722 4723 // Set sizes and widths from page.terms 4724 products.filter.sizes = page.terms.sizes || []; 4725 products.filter.widths = page.terms.widths || []; 4726 4727 // Initialize brands from page.terms.brands (populated during URL parsing) 4728 if (page.terms.brands && page.terms.brands.length > 0) { 4729 products.filter.brands = page.terms.brands.map(b => b.id); 4730 } 4731 4732 // Set genders from page.terms if available (extracted from URL hash) 4733 if (page.terms.genders && page.terms.genders.length > 0) { 4734 products.filter.genders = page.terms.genders; 4735 } 4736 4737 // Track selected colors for client-side filtering (colors from colorDefinitions) 4738 const colorNames = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink', 'beige', 'brown', 'black', 'gray', 'white']; 4739 page.selectedColorFilters = []; 4740 4741 // Collect search terms for brand extraction and synonym expansion 4742 const searchTerms = []; 4743 4744 // Track if we have any quoted phrases from the original search 4745 const quotedPhrasesFromSearch = []; 4746 4747 // Only read from #PillBox (desktop) to avoid duplicates since #MobilePillBox has same pills 4748 $("#PillBox .pill").each(function (i, e) { 4749 var id = $(e).data("id"); 4750 var type = $(e).data("type"); 4751 var isQuoted = $(e).data("quoted") === true; // Check if this pill came from a quoted search 4752 4753 if (type == "brand") { 4754 if (!products.filter.brands.includes(id)) { 4755 products.filter.brands.push(id); 4756 } 4757 } 4758 if (type == "collection") { 4759 // Set single collection (backend only supports one collection at a time) 4760 products.filter.collection = id; 4761 } 4762 if (type == "category") products.filter.category = id; 4763 if (type == "filter") { 4764 // Check if this filter is a color - if so, track for client-side filtering ONLY 4765 const idLower = id.toString().toLowerCase(); 4766 if (colorNames.includes(idLower)) { 4767 // Add to selectedColorFilters if not already present (avoid duplicates) 4768 if (!page.selectedColorFilters.includes(idLower)) { 4769 page.selectedColorFilters.push(idLower); 4770 } 4771 // Don't add colors to search - they're filtered client-side by product.color field 4772 } else if (isQuoted) { 4773 // This is a quoted search phrase - add directly to quotedPhrasesFromSearch without any processing 4774 if (!quotedPhrasesFromSearch.includes(id)) { 4775 quotedPhrasesFromSearch.push(id); 4776 } 4777 } else { 4778 // Non-color, non-quoted filters (like general search terms) - extract quotes first, then process 4779 const quoteResult = extractQuotedStrings(id); 4780 // Add quoted phrases to be used directly in searchVariations 4781 if (quoteResult.quotedPhrases && quoteResult.quotedPhrases.length > 0) {
4782 quoteResult.quotedPhrases.forEach(phrase => { 4783 if (!quotedPhrasesFromSearch.includes(phrase)) { 4784 quotedPhrasesFromSearch.push(phrase); 4785 } 4786 }); 4787 } 4788 // Add remaining text (without quotes) for normal processing 4789 if (quoteResult.cleanedQuery && quoteResult.cleanedQuery.trim().length > 0) { 4790 searchTerms.push(quoteResult.cleanedQuery); 4791 } 4792 } 4793 } 4794 }); 4795 4796 // Check if we only have quoted phrases (no other search terms) 4797 const hasOnlyQuotedPhrases = quotedPhrasesFromSearch.length > 0 && searchTerms.length === 0; 4798 4799 // Extract brands from search terms before synonym expansion (skip if fully quoted) 4800 if (searchTerms.length > 0 && !hasOnlyQuotedPhrases) { 4801 const searchQuery = searchTerms.join(' '); 4802 const brandResult = extractBrandsFromQuery(searchQuery, page.brands); 4803 4804 console.log('Brand extraction result:', { 4805 originalQuery: searchQuery, 4806 detectedBrands: brandResult.detectedBrands, 4807 cleanedQuery: brandResult.cleanedQuery, 4808 currentBrands: products.filter.brands 4809 }); 4810 4811 // Add detected brands to BOTH page.terms.brands (for pills) and products.filter.brands (for API) 4812 if (brandResult.detectedBrands.length > 0) { 4813 if (!page.terms.brands) page.terms.brands = []; 4814 4815 brandResult.detectedBrands.forEach(brand => { 4816 // Add to page.terms.brands if not already there (for pill display) 4817 if (!page.terms.brands.some(b => b.id === brand.id)) { 4818 page.terms.brands.push({ 4819 id: brand.id, 4820 label: brand.name 4821 }); 4822 } 4823 // Add to products.filter.brands for API call 4824 if (!products.filter.brands.includes(brand.id)) { 4825 products.filter.brands.push(brand.id); 4826 } 4827 }); 4828 4829 // Re-render pills to show the new brands 4830 let pillHtml = ""; 4831 if (page.terms.brands && page.terms.brands.length > 0) { 4832 for (let i = 0; i < page.terms.brands.length; i++) { 4833 pillHtml += '<div class="btn btn-sm pill" data-id="' + page.terms.brands[i].id + '" data-type="brand"><i class="fa fa-close"></i> ' + page.terms.brands[i].label + '</div>'; 4834 } 4835 } 4836 // Prepend brand pills to existing pills 4837 $("#PillBox").prepend(pillHtml); 4838 $("#MobilePillBox").prepend(pillHtml); 4839 4840 // Remove the search terms that contained brand names from page.terms.filters 4841 // so they don't show as separate search pills 4842 page.terms.filters = page.terms.filters.filter(f => f.toLowerCase() !== searchQuery.toLowerCase()); 4843 } 4844 4845 // Expand remaining search terms (without brand names) with synonyms 4846 if (brandResult.cleanedQuery && brandResult.cleanedQuery.trim().length > 0) { 4847 const cleanedQuery = brandResult.cleanedQuery.trim(); 4848 const words = cleanedQuery.split(/\s+/); 4849 4850 // Build search variations with synonym expansion for better matching 4851 // Example: "shirts" -> ["shirts", "shirt", "top", "tops", "tee", "tees"] 4852 const synonymExpandedVariations = expandSearchTermsWithSynonyms(words); 4853 4854 // Start with quoted phrases (highest priority - kept as complete phrases) 4855 let searchVariations = []; 4856 if (quotedPhrasesFromSearch && quotedPhrasesFromSearch.length > 0) { 4857 quotedPhrasesFromSearch.forEach(phrase => { 4858 if (phrase && phrase.trim().length > 0) { 4859 searchVariations.push(phrase.trim().toLowerCase()); 4860 } 4861 }); 4862 } 4863 4864 // Then add synonym-expanded variations from remaining text 4865 searchVariations.push(...synonymExpandedVariations); 4866 4867 products.filter.searchVariations = searchVariations; 4868 4869 console.log('Final search variations:', { quotedPhrases: quotedPhrasesFromSearch, synonymExpanded: synonymExpandedVariations, combined: searchVariations }); 4870 4871 // Update page.terms.filters with only the cleaned terms (without brand names) 4872 page.terms.filters = page.terms.filters.filter(f => f.toLowerCase() !== searchQuery.toLowerCase()); 4873 if (!page.terms.filters.includes(cleanedQuery)) { 4874 page.terms.filters.push(cleanedQuery); 4875 } 4876 } else if (quotedPhrasesFromSearch && quotedPhrasesFromSearch.length > 0) { 4877 // If we only have quoted phrases (no other search terms), use them directly 4878 products.filter.searchVariations = quotedPhrasesFromSearch.map(p => p.trim().toLowerCase()); 4879 console.log('Search variations (quoted only):', products.filter.searchVariations); 4880 } else { 4881 delete products.filter.searchVariations; 4882 } 4883 } else if (hasOnlyQuotedPhrases) { 4884 // Fully quoted search - use ONLY the quoted phrases, no brand/synonym expansion 4885 products.filter.searchVariations = quotedPhrasesFromSearch.map(p => p.trim().toLowerCase()); 4886 console.log('Fully quoted search - exact phrases only:', products.filter.searchVariations); 4887 } else { 4888 delete products.filter.searchVariations; 4889 } 4890 4891 delete products.filter.search;
4891 // Remove old search field 4892 4893 $(".pill").off("click", page.removeSearch); 4894 $(".pill").on("click", page.removeSearch); 4895 4896 if ($("#PillBox").html().length>0 || products.filter.sizes.length>0 || products.filter.widths.length>0 || products.filter.prices.length>0) { 4897 if (!page.filtersClosed) { 4898 $("#ClearFilterButton").show(); 4899 $("#PillBoxArea").show(); 4900 } 4901 } else { 4902 $("#ClearFilterButton").hide(); 4903 $("#PillBoxArea").hide(); 4904 } 4905 context.set("storeTerms", page.terms); 4906 try { 4907 // Facebook Pixel tracking - use searchVariations if available, otherwise empty 4908 const searchString = products.filter.searchVariations ? products.filter.searchVariations.join(' ') : ''; 4909 if (searchString.length>0) fbq('track', 'Search', {search_string: searchString}); 4910 } catch (e) {} 4911 if (!page.isInitial) { 4912 // Only add loading state when we're actually going to make an API call 4913 $("#FiltersList").addClass("filter-loading"); 4914 $("#MobileFilterContent").addClass("filter-loading"); 4915 4916 // Show loading state on mobile "View X Products" button 4917 const $applyButton = $("#ApplyMobileFilters"); 4918 if ($applyButton.length) { 4919 $applyButton.addClass('loading').html('Filtering...'); 4920 } 4921 4922 if (page.terms.page != null) products.filter.page = page.terms.page; 4923 4924 // Gender extraction is already handled during synonym expansion 4925 // searchVariations are individual terms, not phrases with gender keywords 4926 4927 // Debug: Log search parameters before API call 4928 console.log('Products filter before API call:', { 4929 brands: products.filter.brands, 4930 searchVariations: products.filter.searchVariations, 4931 fullFilter: products.filter 4932 }); 4933 4934 // When color filters are active, request ALL products for client-side filtering 4935 if (page.selectedColorFilters && page.selectedColorFilters.length > 0) { 4936 products.filter.size = 10000; // Request all products 4937 } 4938 4939 service.call("products-search", products.filter, function(response) { 4940 page.productsLoaded(response); 4941 page.isSearching = false; // Reset flag after search completes 4942 }); 4943 } else { 4944 page.isInitial = false; 4945 page.isSearching = false; // Reset flag 4946 } 4947 }, 4948 updateHash: function() { 4949 const url = new URL(window.location); 4950 url.searchParams.set('c', '1'); 4951 window.history.pushState({}, '', url); 4952 }, 4953 clear: function () { 4954 page.updateHash(); 4955 $("#PillBox").html(""); 4956 $(".searchItem").removeClass("selected"); 4957 $("#BrandCheckboxList input[type='checkbox']").prop("checked", false); 4958 $("#MobileFilterContent #BrandCheckboxList input[type='checkbox']").prop("checked", false); 4959 $(".category-link").removeClass("selected"); 4960 $("#PriceCheckboxList input[type='checkbox']").prop("checked", false); 4961 $("#ShipToMe, #PickupInStore, #OnSale").prop("checked", false); 4962 products.filter.sizes = []; 4963 products.filter.widths = []; 4964 products.filter.prices = []; 4965 products.filter.brands = []; 4966 products.filter.instore = null; 4967 products.filter.onsale = null; 4968 products.filter.shipable = null; 4969 page.forceCategoryId = -1; 4970 // Reset BOTH old and new brand structures to ensure clean state 4971 page.terms = { 4972 brand: { label: "", id: -1 }, 4973 brands: [], 4974 collections: [], 4975 categories: { label: "", id: -1 }, 4976 filter: "", 4977 filters: [], 4978 sizes: [], 4979 widths: [], 4980 prices: [], 4981 colors: [], 4982 genders: [], 4983 instore: 0, 4984 shipable: 0 4985 }; 4986 context.set("storeTerms", page.terms); 4987 window.location = "/shop"; 4988 // page.pillsChanged(); 4989 }, 4990 removeSearch: function (e) { 4991 // page.updateHash(); 4992 var id = $(e.currentTarget).data("id"); 4993 var type = $(e.currentTarget).data("type"); 4994 if (type == "brand") { 4995 // Remove the specific brand from the array 4996 const brandIdToRemove = parseInt(id); 4997 page.terms.brands = page.terms.brands.filter(b => b.id !== brandIdToRemove); 4998 products.filter.brands = page.terms.brands.map(b => b.id); 4999 // Also reset the old singular brand object if this was the only brand 5000 if (page.terms.brands.length === 0) { 5001 page.terms.brand = { label: "", id: -1 }; 5002 } 5003 // Uncheck the specific brand checkbox in both desktop and mobile 5004 $("#FiltersList #BrandCheckboxList input[type='checkbox'][value='" + id + "']").prop("checked", false); 5005 $("#MobileFilterContent #BrandCheckboxList input[type='checkbox'][value='" + id + "']").prop("checked", false); 5006 } else if (type == "collection") { 5007 // Remove the specific collection from the array 5008 const collectionIdToRemove = parseInt(id); 5009 page.terms.collections = page.terms.collections.filter(c => c.id !== collectionIdToRemove); 5010 products.filter.collections = page.terms.collections.map(c => c.id); 5011 // Uncheck the specific collection checkbox in both desktop and mobile 5012 $("#FiltersList #CollectionCheckboxList input[type='checkbox'][value='" + id + "']").prop("checked", false); 5013 $("#MobileFilterContent #CollectionCheckboxList input[type='checkbox'][value='" + id + "']").prop("checked", false); 5014 } else if (type == "category") { 5015 page.terms.categories = { label: "", id: -1 }; 5016 // Remove selected class from category links 5017 $(".category-link").removeClass("selected"); 5018 } else if (type == "size") { 5019 var index = page.terms.sizes.indexOf(id.toString()); 5020 if (index !== -1) { 5021 page.terms.sizes.splice(index, 1); 5022 } 5023 products.filter.sizes = page.terms.sizes;
5024 // Uncheck the size checkbox in both desktop and mobile 5025 $("#FiltersList #SizeCheckboxList input[value='" + id + "']").prop("checked", false); 5026 $("#MobileFilterContent #SizeCheckboxList input[value='" + id + "']").prop("checked", false); 5027 } else if (type == "width") { 5028 var index = page.terms.widths.indexOf(id.toString()); 5029 if (index !== -1) { 5030 page.terms.widths.splice(index, 1); 5031 } 5032 products.filter.widths = page.terms.widths; 5033 // Uncheck the width checkbox in both desktop and mobile 5034 $("#FiltersList #WidthCheckboxList input[value='" + id + "']").prop("checked", false); 5035 $("#MobileFilterContent #WidthCheckboxList input[value='" + id + "']").prop("checked", false); 5036 } else if (type == "price") { 5037 var index = page.terms.prices.indexOf(id.toString()); 5038 if (index !== -1) { 5039 page.terms.prices.splice(index, 1); 5040 } 5041 products.filter.prices = page.terms.prices; 5042 // Uncheck the price checkbox in both desktop and mobile 5043 $("#FiltersList #PriceCheckboxList input[value='" + id + "']").prop("checked", false); 5044 $("#MobileFilterContent #PriceCheckboxList input[value='" + id + "']").prop("checked", false); 5045 } else if (type == "gender") { 5046 var index = page.terms.genders.indexOf(id.toString()); 5047 if (index !== -1) { 5048 page.terms.genders.splice(index, 1); 5049 } 5050 products.filter.genders = page.terms.genders; 5051 // Uncheck the gender checkbox in both desktop and mobile 5052 $("#FiltersList #GenderCheckboxList input[value='" + id + "']").prop("checked", false); 5053 $("#MobileFilterContent #GenderArea input[value='" + id + "']").prop("checked", false); 5054 } else if (type == "option") { 5055 // Uncheck the option checkbox 5056 if (id === "shipable") { 5057 $("#ShipToMe").prop("checked", false); 5058 } else if (id === "instore") { 5059 $("#PickupInStore").prop("checked", false); 5060 } else if (id === "onsale") { 5061 $("#OnSale").prop("checked", false); 5062 } 5063 } else { 5064 var index = page.terms.filters.indexOf(id.toString()); 5065 if (index !== -1) page.terms.filters.splice(index, 1); 5066 5067 // Check if this is a color filter and uncheck the color checkbox 5068 const colorNames = ['black', 'blue', 'green', 'brown', 'white', 'gray', 'red', 'purple', 'pink', 'orange', 'beige', 'yellow']; 5069 if (colorNames.includes(id.toLowerCase())) { 5070 // Find the color with this name (case-insensitive) and uncheck it 5071 $("#FiltersList #ColorCheckboxList input[type='checkbox']").each(function() { 5072 if ($(this).val().toLowerCase() === id.toLowerCase()) { 5073 $(this).prop("checked", false); 5074 } 5075 }); 5076 $("#MobileFilterContent #ColorCheckboxList input[type='checkbox']").each(function() { 5077 if ($(this).val().toLowerCase() === id.toLowerCase()) { 5078 $(this).prop("checked", false); 5079 } 5080 }); 5081 } 5082 } 5083 5084 // Regenerate all pills from page.terms (this removes the clicked pill and keeps everything in sync) 5085 page.pills(); 5086 5087 // Check if all filters have been cleared - if so, redirect to clear URL 5088 const hasAnyFilters = 5089 (page.terms.brands && page.terms.brands.length > 0) || 5090 (page.terms.collections && page.terms.collections.length > 0) || 5091 (page.terms.categories && page.terms.categories.id !== -1) || 5092 (page.terms.sizes && page.terms.sizes.length > 0) || 5093 (page.terms.widths && page.terms.widths.length > 0) || 5094 (page.terms.prices && page.terms.prices.length > 0) || 5095 (page.terms.genders && page.terms.genders.length > 0) || 5096 (page.terms.filters && page.terms.filters.length > 0) || 5097 products.filter.instore || 5098 products.filter.onsale || 5099 products.filter.shipable; 5100 5101 if (!hasAnyFilters) { 5102 // All filters cleared - redirect to clean shop page 5103 window.location = "/shop?c=1"; 5104 return; 5105 } 5106 5107 // Reload products with updated filters
5108 page.pillsChanged(); 5109 }, 5110 updateFilterBadge: function() { 5111 let count = 0; 5112 5113 // Count brands 5114 if (page.terms.brands && page.terms.brands.length > 0) { 5115 count += page.terms.brands.length; 5116 } 5117 5118 // Count category 5119 if (page.terms.categories && page.terms.categories.id != -1) { 5120 count += 1; 5121 } 5122 5123 // Count sizes 5124 if (page.terms.sizes && page.terms.sizes.length > 0) { 5125 count += page.terms.sizes.length; 5126 } 5127 5128 // Count widths 5129 if (page.terms.widths && page.terms.widths.length > 0) { 5130 count += page.terms.widths.length; 5131 } 5132 5133 // Count prices 5134 if (page.terms.prices && page.terms.prices.length > 0) { 5135 count += page.terms.prices.length; 5136 } 5137 5138 // Count options 5139 if ($("input[data-filter='ship-to-me']").first().is(":checked")) { 5140 count += 1; 5141 } 5142 if ($("input[data-filter='pickup-in-store']").first().is(":checked")) { 5143 count += 1; 5144 } 5145 if ($("input[data-filter='on-sale']").first().is(":checked")) { 5146 count += 1; 5147 } 5148 5149 // Count additional filters (excluding brands and categories already counted) 5150 if (page.terms.filters && page.terms.filters.length > 0) { 5151 const oldBrandLabel = (page.terms.brand && page.terms.brand.label) ? page.terms.brand.label.toLowerCase() : ''; 5152 const categoryLabel = (page.terms.categories && page.terms.categories.label) ? page.terms.categories.label.toLowerCase() : ''; 5153 for (var i = 0; i < page.terms.filters.length; i++) { 5154 const term = page.terms.filters[i]; 5155 if (term.toLowerCase()!=oldBrandLabel && term.toLowerCase()!=categoryLabel) { 5156 count += 1; 5157 } 5158 } 5159 } 5160 5161 // Update badge 5162 const badge = $("#FilterBadge"); 5163 if (count > 0) { 5164 badge.text(count).show(); 5165 } else { 5166 badge.hide(); 5167 } 5168 }, 5169 pills: function () { 5170 products.filter.page = 1; 5171 products.filter.size = 36; 5172 var html = ""; 5173 // Show all brand pills 5174 if (page.terms.brands && page.terms.brands.length > 0) { 5175 for (let i = 0; i < page.terms.brands.length; i++) { 5176 html += '<div class="btn btn-sm pill" data-id="' + page.terms.brands[i].id + '" data-type="brand"><i class="fa fa-close"></i> ' + page.terms.brands[i].label + '</div>'; 5177 } 5178 } 5179 // Show all collection pills 5180 if (page.terms.collections && page.terms.collections.length > 0) { 5181 for (let i = 0; i < page.terms.collections.length; i++) { 5182 html += '<div class="btn btn-sm pill" data-id="' + page.terms.collections[i].id + '" data-type="collection"><i class="fa fa-close"></i> ' + page.terms.collections[i].label + '</div>'; 5183 } 5184 } 5185 if (page.terms.categories && page.terms.categories.id != -1) { 5186 html += '<div class="btn btn-sm pill" data-id="' + page.terms.categories.id + '" data-type="category"><i class="fa fa-close"></i> ' + page.terms.categories.label + '</div>'; 5187 $(".subCat"+page.terms.categories.id).show(); 5188 } 5189 if (page.terms.sizes && page.terms.sizes.length > 0) { 5190 for (var i = 0; i < page.terms.sizes.length; i++) { 5191 var size = page.terms.sizes[i]; 5192 html += '<div class="btn btn-sm pill" data-id="' + size + '" data-type="size"><i class="fa fa-close"></i> ' + size + '</div>'; 5193 } 5194 } 5195 if (page.terms.widths && page.terms.widths.length > 0) { 5196 for (var i = 0; i < page.terms.widths.length; i++) { 5197 var width = page.terms.widths[i]; 5198 html += '<div class="btn btn-sm pill" data-id="' + width + '" data-type="width"><i class="fa fa-close"></i> ' + width + '</div>'; 5199 } 5200 } 5201 if (page.terms.prices && page.terms.prices.length > 0) { 5202 for (var i = 0; i < page.terms.prices.length; i++) { 5203 var price = page.terms.prices[i]; 5204 var displayPrice = price.includes('-') ? '$' + price.replace('-', '-$') : '$' + price; 5205 html += '<div class="btn btn-sm pill" data-id="' + price + '" data-type="price"><i class="fa fa-close"></i> ' + displayPrice + '</div>'; 5206 } 5207 } 5208 if (page.terms.genders && page.terms.genders.length > 0) { 5209 const genderLabels = { 'M': 'Mens', 'W': 'Womens', 'U': 'Unisex', 'K': 'Kids' }; 5210 for (var i = 0; i < page.terms.genders.length; i++) { 5211 var gender = page.terms.genders[i];
5212 var displayGender = genderLabels[gender] || gender; 5213 html += '<div class="btn btn-sm pill" data-id="' + gender + '" data-type="gender"><i class="fa fa-close"></i> ' + displayGender + '</div>'; 5214 } 5215 } 5216 // Show pills for options 5217 if ($("input[data-filter='ship-to-me']").first().is(":checked")) { 5218 html += '<div class="btn btn-sm pill" data-id="shipable" data-type="option"><i class="fa fa-close"></i> Ship To Me</div>'; 5219 } 5220 if ($("input[data-filter='pickup-in-store']").first().is(":checked")) { 5221 html += '<div class="btn btn-sm pill" data-id="instore" data-type="option"><i class="fa fa-close"></i> Pickup In-Store</div>'; 5222 } 5223 if ($("input[data-filter='on-sale']").first().is(":checked")) { 5224 html += '<div class="btn btn-sm pill" data-id="onsale" data-type="option"><i class="fa fa-close"></i> On Sale</div>'; 5225 } 5226 if (page.terms.filters && page.terms.filters.length > 0) { 5227 for (var i = 0; i < page.terms.filters.length; i++) { 5228 var term = page.terms.filters[i]; 5229 const oldBrandLabel = (page.terms.brand && page.terms.brand.label) ? page.terms.brand.label.toLowerCase() : ''; 5230 const categoryLabel = (page.terms.categories && page.terms.categories.label) ? page.terms.categories.label.toLowerCase() : ''; 5231 if (term.toLowerCase()!=oldBrandLabel && term.toLowerCase()!=categoryLabel) { 5232 // Check if this filter came from a quoted search 5233 const isQuoted = page.terms.quotedFilters && page.terms.quotedFilters.includes(term); 5234 const quotedAttr = isQuoted ? ' data-quoted="true"' : ''; 5235 html += '<div class="btn btn-sm pill" data-id="' + page.terms.filters[i].split('"').join('') + '" data-type="filter"' + quotedAttr + '><i class="fa fa-close"></i> ' + term + '</div>'; 5236 } 5237 } 5238 } 5239 $("#PillBox").html(html); 5240 $("#MobilePillBox").html(html); 5241 5242 // Re-attach click handlers to pills after updating HTML (both desktop and mobile) 5243 $("#PillBox .pill").off("click", page.removeSearch); 5244 $("#PillBox .pill").on("click", page.removeSearch); 5245 $("#MobilePillBox .pill").off("click", page.removeSearch); 5246 $("#MobilePillBox .pill").on("click", page.removeSearch); 5247 5248 // Show/hide mobile pill box area based on whether there are pills 5249 if (html && html.trim().length > 0) { 5250 $("#MobilePillBoxArea").show(); 5251 } else { 5252 $("#MobilePillBoxArea").hide(); 5253 } 5254 5255 page.updateFilterBadge(); 5256 page.pillsChanged(); 5257 }, 5258 addSearch: function (label, id, from, doSearch = true) { 5259 page.lastSearch = label; 5260 if (from == "filter") { 5261 if (typeof products.searchView === 'function') { 5262 products.searchView(label); 5263 } 5264 if (!page.findBrand(label)) { 5265 page.findCategory(label); 5266 } 5267 } else if (from == "brand") { 5268 page.terms.brand = { 5269 label: label, 5270 id: id 5271 } 5272 // Only navigate to brand page if we're on a forced brand page and switching to a SINGLE different brand 5273 // Don't navigate if we're in multi-brand mode (already have brands selected via checkboxes) 5274 const hasMultipleBrands = page.terms.brands && page.terms.brands.length > 0; 5275 if (!hasMultipleBrands && page.forceBrandId !=null && page.forceBrandId!=-1 && id!=page.forceBrandId) { 5276 doSearch = false; 5277 var url = label.trim().toLowerCase().replace(/\W/g, '-'); 5278 while (url.indexOf("--")>=0) url = url.split('--').join('-'); 5279 window.location = "/brand/"+id+"/"+url; 5280 } 5281 } else if (from == "category") { 5282 page.terms.categories = { 5283 label: label, 5284 id: id 5285 } 5286 // Only navigate to category page if we're switching to a SINGLE different category 5287 // Don't navigate if we're in multi-filter mode 5288 const hasMultipleCategories = page.terms.categories && page.terms.categories.length > 0;
5289 if (!hasMultipleCategories && page.forceCategoryId!=null && page.forceCategoryId!=-1 && id!=page.forceCategoryId) { 5290 doSearch = false; 5291 var url = label.trim().toLowerCase().replace(/\W/g, '-'); 5292 while (url.indexOf("--")>=0) url = url.split('--').join('-'); 5293 window.location = "/category/"+id+"/"+url; 5294 } 5295 } else if (from == "size") { 5296 // Size is already added to page.terms.sizes array in the checkbox handler 5297 // Just trigger the pills refresh 5298 } else if (from == "width") { 5299 // Width is already added to page.terms.widths array in the checkbox handler 5300 // Just trigger the pills refresh 5301 } else if (from == "price") { 5302 // Price is already added to page.terms.prices array in the checkbox handler 5303 // Just trigger the pills refresh 5304 } else if (from == "collection") { 5305 // Collection is already added to page.terms.collections array in the checkbox handler 5306 // Just trigger the pills refresh 5307 } else if (from == "gender") { 5308 // Gender is already added to page.terms.genders array in the checkbox handler 5309 // Just trigger the pills refresh 5310 } 5311 if (doSearch) { 5312 console.log("Adding search term from "+from+": "+label); 5313 page.terms.page = 1; 5314 page.pills(); 5315 } 5316 }, 5317 findCategory: function(searchFor) { 5318 var found = false; 5319 searchFor = searchFor.trim().toLowerCase(); 5320 if (!page.terms.filters) page.terms.filters = []; 5321 if (searchFor.length>0) { 5322 for (var i = 0; i < page.categories.length; i++) { 5323 const category = page.categories[i]; 5324 const categoryName = category.name ? category.name.toLowerCase() : ''; 5325 const categoryLabel = category.label ? category.label.toLowerCase() : ''; 5326 5327 if (categoryName === searchFor || categoryLabel === searchFor) { 5328 page.terms.categories.label = category.label || category.name; 5329 page.terms.categories.id = category.categoryId; 5330 found = true; 5331 break; 5332 } 5333 } 5334 } 5335 if (!found) { 5336 if (searchFor.length > 0 && !page.IsInArray(searchFor, page.terms.filters)) { 5337 page.terms.filters[page.terms.filters.length] = searchFor; 5338 } 5339 } 5340 return found; 5341 }, 5342 IsInArray: function(str, arr) { 5343 str = str.trim().toLowerCase(); 5344 for (var i=0; i<arr.length; i++) { 5345 let item = arr[i].trim().toLowerCase(); 5346 if (item==str) return true; 5347 } 5348 return false; 5349 }, 5350 findBrand: function (searchFor) { 5351 var found = false; 5352 searchFor = searchFor.trim().toLowerCase(); 5353 if (!page.terms.filters) page.terms.filters = []; 5354 for (var i = 0; i < page.brands.length; i++) { 5355 if (page.brands[i].name.toLowerCase()==searchFor) { 5356 page.terms.brand = { 5357 label: page.brands[i].name, 5358 id: page.brands[i].brandId 5359 } 5360 found = true; 5361 break; 5362 } 5363 } 5364 return found; 5365 }, 5366 sameString: function (s1, s2) { 5367 var samAs = ''; 5368 for (var i = 0; i < s1.length; i++) { 5369 if (s2.length > i) { 5370 if (s1[i] == s2[i]) samAs += s1[i]; 5371 else return samAs; 5372 } 5373 } 5374 return samAs; 5375 }, 5376 productsLoaded: function (e) { 5377 // Remove loading state from filters 5378 $("#FiltersList").removeClass("filter-loading"); 5379 $("#MobileFilterContent").removeClass("filter-loading"); 5380 5381 $("#SearchFilterArea").removeClass("disabled"); 5382 $("#BrandingImage").hide(); 5383 $("#ProductList").html(""); 5384 5385 // Apply client-side color filtering if colors are selected 5386 let filteredData = e.data; 5387 if (page.selectedColorFilters && page.selectedColorFilters.length > 0) { 5388 filteredData = e.data.filter(product => 5389 page.productMatchesColorFilter(product.color, page.selectedColorFilters) 5390 ); 5391 // Update total count to reflect filtered results
5392 e.total = filteredData.length; 5393 } 5394 5395 page.products = filteredData; 5396 products.data = filteredData; 5397 products.total = e.total; 5398 5399 // Update both brand and option counts with a single API call 5400 page.updateFilterCounts(); 5401 5402 // Update available sizes based on filtered products 5403 // page.updateAvailableSizes(); 5404 5405 // Update mobile filter pills and product count if panel is open 5406 if ($("#MobileFilterPanel").hasClass('active')) { 5407 page.syncMobilePills(); 5408 page.updateMobileProductCount(); 5409 5410 // Ensure all mobile filter sections remain visible after updates 5411 // This fixes the issue where filters collapse after checkbox changes 5412 setTimeout(function() { 5413 // Make sure the main FiltersList container is visible 5414 $('#MobileFilterContent #FiltersList').show(); 5415 // Force every accordion section open/visible by both removing closed class AND setting display 5416 $('#MobileFilterContent .filter-container').removeClass('closed').css('display', 'block'); 5417 $('#MobileFilterContent .toggleright').removeClass('closed'); 5418 $('#MobileFilterContent .filter-content').show().css('display', 'block'); 5419 $('#MobileFilterContent #CategoryList').show(); 5420 // Keep the user at the top of the list so content is visible 5421 const mobileContent = document.querySelector('.mobile-filter-content'); 5422 if (mobileContent) mobileContent.scrollTop = 0; 5423 }, 100); 5424 } 5425 5426 // Sync checkbox checked states for all active filters 5427 page.terms?.brands?.forEach(brand => { 5428 if (brand.id) page.syncCheckboxes('brand-filter', brand.id.toString()); 5429 }); 5430 page.terms?.sizes?.forEach(size => { 5431 page.syncCheckboxes('size-filter', size); 5432 }); 5433 page.terms?.widths?.forEach(width => { 5434 page.syncCheckboxes('width-filter', width); 5435 }); 5436 // Sync gender checkboxes (pills are managed separately by hashChanged and checkbox handlers) 5437 if (page.terms.genders && page.terms.genders.length > 0) { 5438 page.terms.genders.forEach(gender => { 5439 $('#FiltersList #GenderCheckboxList input[value="' + gender + '"]').prop('checked', true); 5440 $('#MobileFilterContent #GenderArea input[value="' + gender + '"]').prop('checked', true); 5441 }); 5442 } 5443 //page.products = context.get("products"+products.searchId); 5444 page.category = category.details(products.filter.category); 5445 if (page.category != null) { 5446 let categoryLabel = page.category.name; 5447 if (page.category.label) categoryLabel = page.category.label; 5448 $("#CategoryName").html(categoryLabel); 5449 } 5450 else $("#CategoryName").html("All Categories"); 5451 if (page.products.length > 0) { 5452 let productIds = []; 5453 // Only use server total if no client-side color filtering is active 5454 if (!page.selectedColorFilters || page.selectedColorFilters.length === 0) { 5455 products.total = page.products[0].totalRows; 5456 } 5457 for (var i = 0; i < page.products.length; i++) { 5458 var element = $("#RowTemplate").clone(); 5459 element.removeClass("template"); 5460 element.css("display", "inline"); 5461 element.attr("id", "Product" + i); 5462 5463 var brandLabel = page.products[i].brandName; 5464 if (page.products[i].label&&page.products[i].label.length>0) brandLabel = page.products[i].label; 5465 5466 var url = "/product/" + page.products[i].productId + "/" + page.products[i].name.trim().toLowerCase().replace(/\W/g, '-'); 5467 url = "/product/" + page.products[i].productId + "/" + brandLabel.trim().toLowerCase().replace(/\W/g, '-') + "/" + page.products[i].name.trim().toLowerCase().split('\'').join('').replace(/\W/g, '-'); 5468 5469 element.attr("data-url", url); 5470 5471 // Update wishlist button productId before string replacements 5472 element.find('.wishlist-toggle').attr('data-product-id', page.products[i].productId); 5473 5474 // Update image, name, brand, etc using direct DOM manipulation where possible 5475 element.find('.image').css('background-image', 'url("' + products.getSize(380, page.products[i].url) + '")'); 5476 element.find('.name').text(page.products[i].name); 5477 element.find('.brand').text(brandLabel); 5478 element.find('a[rel="cononical"]').attr('href', url).attr('aria-label', page.products[i].name); 5479 5480 // Update color swatches data attributes 5481 element.find('.colorswatches') 5482 .attr('data-product-id', page.products[i].productId) 5483 .attr('data-product-name', page.products[i].name) 5484 .attr('data-site-id', site.id) 5485 .attr('data-total-colors', page.products[i].totalColors || 1); 5486 5487 5488 if (page.products[i].markdown!=null && page.products[i].markdown>0 && page.products[i].isAuto) { 5489 page.products[i].realcost = page.products[i].cost; 5490 page.products[i].cost = page.products[i].markdown; 5491 } 5492 var onsale = ""; 5493 var retail = page.products[i].retail; 5494 if (retail > page.products[i].cost) { 5495 var htmlString = '<span class="struck">$' + retail.toFixed(2) + '</span> $' + page.products[i].cost.toFixed(2); 5496 element.find('.price').html(htmlString); 5497 if (page.products[i].isOnSale || isAutoSale) onsale = " show"; 5498 } else { 5499 element.find('.price').text("$" + page.products[i].cost.toFixed(2)); 5500 if (page.products[i].isOnSale) onsale = " show"; 5501 } 5502 element.find('.salelabel').addClass(onsale.trim()); 5503 var releaseLabel = productRelease.getAvailabilityLabel(page.products[i].dateReleased); 5504 element.find('.releaselabel').text(releaseLabel).toggleClass('show', releaseLabel.length > 0); 5505 $("#ProductList").append(element); 5506 productIds.push(page.products[i].sku); 5507 } 5508 5509 try { 5510 var gTagListItems = []; 5511 var listName = (page.category && (page.category.label || page.category.name)) || "Shop"; 5512 var listId = (page.category && page.category.categoryId) ? String(page.category.categoryId) : "shop"; 5513 for (var j = 0; j < page.products.length; j++) { 5514 if (!page.products[j]) continue; 5515 gTagListItems.push({ 5516 "id": page.products[j].productId, 5517 "name": page.products[j].name, 5518 "list_name": listName, 5519 "brand": page.products[j].brandName, 5520 "category": page.products[j].categories && page.products[j].categories[0] ? page.products[j].categories[0].name : 'All Categories', 5521 "variant": page.products[j].color, 5522 "list_position": j, 5523 "price": page.products[j].cost 5524 }); 5525 } 5526 if (gTagListItems.length > 0) { 5527 trackEvent('view_item_list', { 5528 "item_list_id": listId, 5529 "item_list_name": listName, 5530 "items": gTagListItems 5531 }); 5532 } 5533 } catch (e) {} 5534 5535 // Update wishlist button states for all products 5536 page.updateWishlistButtons(); 5537 5538 $("#PagingTotals").show(); 5539 $("#PagingTotalsNone").hide(); 5540 page.pageLoaded(); 5541 if (!isNaN(products.filter.brand) && Number(products.filter.brand) > 0) { 5542 var brandData = brand.details(products.filter.brand); 5543 if (brandData.image && brandData.image.indexOf("NoImage.svg") == -1) { 5544 $("#BrandImage").prop("src", brandData.image); 5545 $("#BrandingImage").show(); 5546 } 5547 } else if (brandImage && brandImage.trim() != '' && brandImage.indexOf("NoImage.svg") == -1) { 5548 $("#BrandImage").prop("src", brandImage); 5549 $("#BrandingImage").show(); 5550 } 5551 if (page.lastSearch && typeof page.lastSearch === 'string' && page.lastSearch.trim().length>0) { 5552 if (window.blueshift!=null) { 5553 blueshift.track("search", { 5554 keywords: page.lastSearch, 5555 product_ids: productIds 5556 }); 5557 } 5558 } 5559 } else { 5560 $("#PagingArea").hide(); 5561 $("#PagingTotals").hide(); 5562 $("#PagingTotalsNone").show(); 5563 5564 $("#ProductList").html( 5565 "<style>#ProductList.grid.responsive-product-grid {grid-template-columns: 100%;} </style><div class='hidemobile'></div><div style='text-align: center; color: #d2d8e5; font-size: 16px; font-weight: 600;'><div class='sadsearch'></div><span style='font-size: 32px;'>Hmmm... <br><span style='font-size: 26px;'>We didn't find any matches</span></span><br>
5565Modify your search filters and try again</div><div class='hidemobile'></div>" 5566 ); 5567 5568 // Update mobile filter button to show "No Results" when there are no products 5569 const $applyButton = $("#ApplyMobileFilters"); 5570 if ($applyButton.length && $applyButton.hasClass('loading')) { 5571 $applyButton.removeClass('loading').html('No Results'); 5572 } 5573 } 5574 5575 const pageTotal = typeof products.pageTotal === 'function' ? products.pageTotal() : Math.ceil(products.total / products.filter.size); 5576 if (pageTotal > 1) { 5577 $(".pages").html(""); 5578 $("#PagingArea").show(); 5579 const isFirst = typeof products.isFirst === 'function' ? products.isFirst() : (products.filter.page === 1); 5580 const isLast = typeof products.isLast === 'function' ? products.isLast() : (products.filter.page === pageTotal); 5581 if (!isFirst) 5582 $(".pages").append('<div class="page" data-page="' + (products.filter.page - 1) + '"><</div>'); 5583 var startIndex = 1; 5584 if (products.filter.page > 5) startIndex = products.filter.page - 5; 5585 var total = startIndex + 10; 5586 for (var i = startIndex; i <= total && i <= pageTotal; i++) { 5587 $(".pages").append('<div class="page' + (products.filter.page == i ? " current" : "") + '" data-page="' + i + '">' + i + "</div>"); 5588 } 5589 if (!isLast) $(".pages").append('<div class="page" data-page="' + (products.filter.page + 1) + '">></div>'); 5590 $(".page").click(page.page); 5591 $(".paging").show(); 5592 $(".pageCount").show(); 5593 } else { 5594 $("#PagingArea").hide(); 5595 $(".paging").hide(); 5596 $(".pageCount").hide(); 5597 } 5598 5599 // Remove initializing state after filtered products are loaded 5600 $('#ProductList').removeClass('initializing'); 5601 $('#InitialLoader').remove(); 5602 $('.search-page-container').removeClass('initializing'); 5603 }, 5604 pageLoaded: function () { 5605 // Attach click handler to .clickable for navigation 5606 // The goto function checks for wishlist button and swatch clicks 5607 $(".clickable").click(page.goto); 5608 5609 // Attach wishlist toggle handlers directly (must be after .clickable to fire first) 5610 $('.wishlist-toggle').off('click').on('click', page.toggleWishlist); 5611 5612 $("#SearchTotal").html(products.total); 5613 $("#SearchCurrentPage").html(products.filter.page); 5614 $("#SearchShowing").html(((products.filter.page - 1) * products.filter.size) + 1); 5615 const pageTotal = typeof products.pageTotal === 'function' ? products.pageTotal() : Math.ceil(products.total / products.filter.size); 5616 $("#SearchTotalPages").html(pageTotal); 5617 $("#SearchTo").html(((products.filter.page - 1) * products.filter.size) + products.data.length); 5618 5619 // Restore mobile "View X Products" button (after #SearchTotal is updated!) 5620 const $applyButton = $("#ApplyMobileFilters"); 5621 if ($applyButton.length && $applyButton.hasClass('loading')) { 5622 $applyButton.removeClass('loading'); 5623 // Read the fresh product count that was just set 5624 const count = parseInt(products.total, 10) || 0; 5625 if (count === 0) { 5626 $applyButton.html('No Results'); 5627 } else { 5628 $applyButton.html('View <span id="MobileFilterCount">' + count + '</span> Products'); 5629 } 5630 } 5631 5632 // Update mobile filter count 5633 page.updateMobileProductCount(); 5634 5635 // Apply stagger animation to dynamically loaded products 5636 document.querySelectorAll('.product').forEach((el, i) => el.style.setProperty('--i', i + 1)); 5637 // Load color swatches for products with multiple colors 5638 colorSwatches.init('#ProductList'); 5639 colorSwatches.load(); 5640 5641 // Re-render swatches after layout is complete to get accurate container widths 5642 setTimeout(function() { 5643 colorSwatches.load(true); // Force refresh with correct widths 5644 }, 300); 5645 }, 5646 updateWishlistButtons: function() { 5647 // Update wishlist button states for all visible products
5648 if (!wishlist || !wishlist.isLoaded) { 5649 return; 5650 } 5651 5652 $('#ProductList .wishlist-toggle').each(function() { 5653 const $button = $(this); 5654 const productId = parseInt($button.attr('data-product-id')); 5655 5656 if (wishlist.has(productId)) { 5657 $button.removeClass('inactive').addClass('active'); 5658 $button.attr('aria-label', 'Remove from wishlist'); 5659 } else { 5660 $button.removeClass('active').addClass('inactive'); 5661 $button.attr('aria-label', 'Add to wishlist'); 5662 } 5663 }); 5664 }, 5665 toggleWishlist: function(e) { 5666 e.stopPropagation(); 5667 e.stopImmediatePropagation(); 5668 e.preventDefault(); 5669 5670 // Check if user is logged in 5671 if (!user || !user.data || !user.data.memberId) { 5672 modal.show('WishlistLoginModal'); 5673 return; 5674 } 5675 5676 const $button = $(e.currentTarget); 5677 const productId = parseInt($button.attr('data-product-id')); 5678 5679 // Remove focus to hide tooltip immediately 5680 $button.blur(); 5681 5682 // Handle deactivate animation 5683 if ($button.hasClass("deactivate")) { 5684 $button.removeClass("deactivate"); 5685 } 5686 if ($button.hasClass("active")) { 5687 $button.addClass("deactivate"); 5688 } 5689 5690 wishlist.toggle(productId, function(response) { 5691 if (response.error) { 5692 growler.error("Failed to update wishlist"); 5693 } else { 5694 if (response.isAdded) { 5695 $button.removeClass("inactive deactivate").addClass("active"); 5696 $button.attr("aria-label", "Remove from wishlist"); 5697 growler.success("Added to wishlist"); 5698 } else { 5699 // Deactivate animation will run, then switch to inactive 5700 setTimeout(function() { 5701 $button.removeClass("active deactivate").addClass("inactive"); 5702 $button.attr("aria-label", "Add to wishlist"); 5703 }, 800); 5704 growler.success("Removed from wishlist"); 5705 } 5706 } 5707 }); 5708}, 5709 page: function (e) { 5710 $("#ProductList").html($("#CardLoader").html()); 5711 var pageTo = $(e.currentTarget).data("page"); 5712 if (!$(e.currentTarget).hasClass("current")) { 5713 page.terms.page = pageTo; 5714 context.set("storeTerms", page.terms); 5715 products.filter.page = pageTo; 5716 service.call("products-search", products.filter, page.productsLoaded); 5717 $("html, body").animate({ scrollTop: "296px" }); 5718 } 5719 }, 5720 goto: function (e) { 5721 // Don't navigate if the click was on the wishlist button or color swatch 5722 // Check both e.target and any parent elements 5723 const $target = $(e.target); 5724 if ($target.closest('.wishlist-toggle, .colorswatch').length > 0 || 5725 $target.hasClass('wishlist-toggle') || 5726 $target.parents('.wishlist-toggle').length > 0) { 5727 e.stopPropagation(); 5728 e.preventDefault(); 5729 return false; 5730 } 5731 window.location = $(e.currentTarget).data("url"); 5732 }, 5733 getChildren: function(parentId) { 5734 return page.categories.filter(cat => cat.parentId === parentId); 5735 }, 5736 toggleCategoryChildren: function(e) { 5737 e.stopPropagation(); 5738 const categoryId = $(e.currentTarget).data('parent-id'); 5739 const $icon = $(e.currentTarget); 5740 // Find the child container within the same parent context (mobile or desktop) 5741 const $childContainer = $icon.closest('.category-item').find('#category-children-' + categoryId); 5742 5743 if ($icon.hasClass('fa-minus')) { 5744 // Currently expanded, so collapse 5745 $childContainer.slideUp(200); 5746 $icon.removeClass('fa-minus').addClass('fa-plus'); 5747 } else { 5748 // Currently collapsed, so expand 5749 $childContainer.slideDown(200); 5750 $icon.removeClass('fa-plus').addClass('fa-minus'); 5751 } 5752 }, 5753 categoriesLoaded: function (e) { 5754 page.categories = e; 5755 5756 // Sort categories alphabetically by label (or name if no label) 5757 page.categories.sort((a, b) => { 5758 const nameA = (a.label && a.label.trim().length > 0 ? a.label : a.name).toLowerCase(); 5759 const nameB = (b.label && b.label.trim().length > 0 ? b.label : b.name).toLowerCase(); 5760 return nameA.localeCompare(nameB); 5761 }); 5762 5763 // Filter out categories with 0 count and hidden categories 5764 page.categories = page.categories.filter(cat => (cat.total || 0) > 0 && !cat.isHidden && !cat.hidden); 5765 5766 // Get parent categories 5767 page.parentCategories = page.categories.filter(cat => cat.parentId === -1); 5768 5769 // Store all categories with their data 5770 page.allCategories = page.categories.map(cat => ({ 5771 categoryId: cat.categoryId, 5772 name: cat.name, 5773 label: cat.label, 5774 parentId: cat.parentId,
5775 productCount: cat.total || 0 5776 })); 5777 5778 // Handle forced category selection 5779 if (page.categories.length > 0) { 5780 for (var i = 0; i < page.categories.length; i++) { 5781 if (page.forceCategoryId > 0) { 5782 if (page.forceCategoryId == page.categories[i].categoryId) { 5783 page.terms.categories.label = page.categories[i].label; 5784 page.terms.categories.id = page.categories[i].categoryId; 5785 } 5786 } else { 5787 if (page.forceCategory != "" && (page.categories[i].name.toLowerCase() == page.forceCategory.toLowerCase() || page.categories[i].name.split(' ').join('').toLowerCase() == page.forceCategory.split(' ').join(''))) { 5788 page.terms.categories.label = page.categories[i].label; 5789 page.terms.categories.id = page.categories[i].categoryId; 5790 page.forceCategoryId = page.categories[i].categoryId; // Set the ID so filters are applied 5791 } 5792 } 5793 } 5794 } 5795 5796 // Render category checkboxes 5797 page.renderCategoryCheckboxes(); 5798 5799 // Setup show more functionality 5800 $("#CategoryShowMore a").off("click").on("click", function(e) { 5801 e.preventDefault(); 5802 page.showAllCategories(); 5803 }); 5804 5805 // Setup category search functionality 5806 $("#CategorySearchInput").off("input").on("input", function() { 5807 page.filterCategoryLinks($(this).val()); 5808 }); 5809 5810 page.category = category.details(products.filter.category); 5811 if (page.category != null) $("#CategoryName").html(page.category.name); 5812 else $("#CategoryName").html("All Categories"); 5813 5814 if (page.doAfterCatLoad) { 5815 page.urlParamsChanged(); 5816 // After processing URL params with loaded categories, render pills if needed 5817 if (!(page.forceCategory != "" || page.forceCategoryId > 0)) { 5818 page.pills(); 5819 } 5820 } 5821 5822 if (page.forceCategory != "" || page.forceCategoryId > 0) { 5823 // Reset search state since we now have the category ID resolved 5824 // This ensures the API call includes the category filter 5825 page.isSearching = false; 5826 page.pills(); 5827 } 5828 5829 if (page.categories.length > 0) { 5830 $("#CategoryList").show(); 5831 } else { 5832 $("#CategoryList").hide(); 5833 } 5834 }, 5835 renderCategoryCheckboxes: function(showAll, searchTerm, matchingChildIds) { 5836 const parentCategories = page.parentCategories; 5837 const showCount = showAll ? parentCategories.length : 10; 5838 const displayCategories = parentCategories.slice(0, showCount); 5839 const remainingCount = parentCategories.length - showCount; 5840 5841 // Default to empty if not provided 5842 searchTerm = searchTerm || ''; 5843 matchingChildIds = matchingChildIds || new Set(); 5844 5845 let html = ''; 5846 for (let i = 0; i < displayCategories.length; i++) { 5847 const cat = displayCategories[i]; 5848 const isSelected = page.terms.categories.id === cat.categoryId; 5849 const label = cat.label && cat.label.trim().length > 0 ? cat.label.trim() : cat.name; 5850 const countDisplay = "";//` (${cat.total || 0})`; 5851 const children = page.getChildren(cat.categoryId); 5852 const hasChildren = children.length > 0; 5853 5854 html += '<div class="category-item">'; 5855 5856 if (hasChildren) { 5857 // Auto-expand if searching (show toggle as minus) 5858 const expandClass = searchTerm ? 'fa-minus' : 'fa-plus'; 5859 const expandStyle = searchTerm ? 'display:block;' : 'display:none;'; 5860 html += `<i class="fa ${expandClass} category-toggle" data-parent-id="${cat.categoryId}"></i><a href="javascript:void(0);" class="category-link ${isSelected ? 'selected' : ''}" data-category-id="${cat.categoryId}" data-category-name="${label}">${label}</a>`; 5861 } else { 5862 html += `<span class="category-spacer"></span><a href="javascript:void(0);" class="category-link ${isSelected ? 'selected' : ''}" data-category-id="${cat.categoryId}" data-category-name="${label}">${label}</a>`; 5863 } 5864 5865 if (hasChildren) { 5866 // Filter children to only show matching ones (or all if parent directly matched) 5867 const parentName = (cat.label && cat.label.trim().length > 0 ? cat.label : cat.name).toLowerCase();
5868 const parentDirectlyMatched = searchTerm && parentName.includes(searchTerm.toLowerCase()); 5869 5870 // If parent directly matched, show all children. Otherwise, only show matching children. 5871 const childrenToShow = !searchTerm || parentDirectlyMatched 5872 ? children 5873 : children.filter(child => matchingChildIds.has(child.categoryId)); 5874 5875 if (childrenToShow.length > 0) { 5876 // Show children expanded when searching 5877 const childStyle = searchTerm ? 'display:block;' : 'display:none;'; 5878 html += `<div id="category-children-${cat.categoryId}" class="category-children" style="${childStyle}">`; 5879 5880 // Sort children alphabetically 5881 childrenToShow.sort((a, b) => { 5882 const nameA = (a.label && a.label.trim().length > 0 ? a.label : a.name).toLowerCase(); 5883 const nameB = (b.label && b.label.trim().length > 0 ? b.label : b.name).toLowerCase(); 5884 return nameA.localeCompare(nameB); 5885 }); 5886 5887 for (let j = 0; j < childrenToShow.length; j++) { 5888 const child = childrenToShow[j]; 5889 const childLabel = child.label && child.label.trim().length > 0 ? child.label.trim() : child.name; 5890 const childIsSelected = page.terms.categories.id === child.categoryId; 5891 5892 html += `<div class="category-child-item"><a href="javascript:void(0);" class="category-link ${childIsSelected ? 'selected' : ''}" data-category-id="${child.categoryId}" data-category-name="${childLabel}">${childLabel}</a></div>`; 5893 } 5894 5895 html += '</div>'; 5896 } 5897 } 5898 5899 html += '</div>'; 5900 } 5901 5902 // Update both desktop and mobile (if mobile panel is open) 5903 $("#FiltersList #CategoryLinkList").html(html); 5904 $("#MobileFilterContent #CategoryLinkList").html(html); 5905 5906 // Toggle "Show more/less" state (both desktop and mobile) 5907 if (showAll) { 5908 $("#FiltersList #CategoryShowMore a").html('Show less'); 5909 $("#MobileFilterContent #CategoryArea #CategoryShowMore a").html('Show Less'); 5910 $("#FiltersList #CategoryShowMore").show(); 5911 $("#MobileFilterContent #CategoryArea #CategoryShowMore").show(); 5912 } else if (remainingCount > 0) { 5913 $("#CategoryShowMoreCount").text(remainingCount); 5914 $("#FiltersList #CategoryShowMore a").html(`Show <span>${remainingCount}</span> more`); 5915 $("#MobileFilterContent #CategoryArea #CategoryShowMoreCount").text(remainingCount); 5916 $("#MobileFilterContent #CategoryArea #CategoryShowMore a").html(`Show <span>${remainingCount}</span> More`); 5917 $("#FiltersList #CategoryShowMore").show(); 5918 $("#MobileFilterContent #CategoryArea #CategoryShowMore").show(); 5919 } else { 5920 $("#FiltersList #CategoryShowMore").hide(); 5921 $("#MobileFilterContent #CategoryArea #CategoryShowMore").hide(); 5922 } 5923 5924 // Re-attach handlers 5925 $(".category-toggle").off("click").on("click", page.toggleCategoryChildren); 5926 $(".category-link").off("click").on("click", function() { 5927 const categoryId = parseInt($(this).data("category-id")); 5928 const categoryName = $(this).data("category-name"); 5929 $(".category-link").removeClass("selected"); 5930 $(this).addClass("selected"); 5931 page.terms.categories.id = categoryId; 5932 page.terms.categories.label = categoryName; 5933 page.addPill(categoryId, categoryName, "category"); 5934 products.filter.category = page.terms.categories.id; 5935 }); 5936 }, 5937 showAllCategories: function() { 5938 // Check both desktop and mobile buttons to determine state 5939 const desktopText = $("#FiltersList #CategoryShowMore a").text().trim(); 5940 const mobileText = $("#MobileFilterContent #CategoryShowMore a").text().trim(); 5941 const isShowingAll = desktopText === 'Show less' || mobileText === 'Show Less'; 5942 page.renderCategoryCheckboxes(!isShowingAll); 5943 }, 5944 filterCategoryLinks: function(searchTerm) { 5945 // If search is empty, show all categories normally 5946 if (!searchTerm || searchTerm.trim() === '') { 5947 page.renderCategoryCheckboxes(); 5948 return; 5949 } 5950 5951 const term = searchTerm.toLowerCase().trim(); 5952 5953 // Find matching parent IDs - parents whose name matches OR who have a matching child 5954 const matchingParentIds = new Set(); 5955 const matchingChildIds = new Set(); 5956 5957 // Check all categories
5958 page.allCategories.forEach(cat => { 5959 const name = (cat.label && cat.label.trim().length > 0 ? cat.label : cat.name).toLowerCase(); 5960 if (name.includes(term)) { 5961 if (cat.parentId === -1) { 5962 // It's a parent - add it 5963 matchingParentIds.add(cat.categoryId); 5964 } else { 5965 // It's a child - add it AND its parent 5966 matchingChildIds.add(cat.categoryId); 5967 matchingParentIds.add(cat.parentId); 5968 } 5969 } 5970 }); 5971 5972 // Also show all children of matching parents (for context) 5973 page.allCategories.forEach(cat => { 5974 if (cat.parentId !== -1 && matchingParentIds.has(cat.parentId)) { 5975 // Check if the PARENT itself matched (not just got added because of a child) 5976 const parent = page.allCategories.find(p => p.categoryId === cat.parentId); 5977 if (parent) { 5978 const parentName = (parent.label && parent.label.trim().length > 0 ? parent.label : parent.name).toLowerCase(); 5979 if (parentName.includes(term)) { 5980 // Parent directly matched, show all its children 5981 matchingChildIds.add(cat.categoryId); 5982 } 5983 } 5984 } 5985 }); 5986 5987 // Filter parent categories to only matching ones 5988 const filteredParents = page.parentCategories.filter(cat => matchingParentIds.has(cat.categoryId)); 5989 5990 // Render filtered results 5991 page.renderFilteredCategories(filteredParents, matchingChildIds, term); 5992 }, 5993 renderFilteredCategories: function(filteredParents, matchingChildIds, searchTerm) { 5994 let html = ''; 5995 5996 for (let i = 0; i < filteredParents.length; i++) { 5997 const cat = filteredParents[i]; 5998 const isSelected = page.terms.categories.id === cat.categoryId; 5999 const label = cat.label && cat.label.trim().length > 0 ? cat.label.trim() : cat.name; 6000 const children = page.getChildren(cat.categoryId); 6001 const hasChildren = children.length > 0; 6002 6003 html += '<div class="category-item">'; 6004 6005 if (hasChildren) { 6006 // Auto-expand if searching (show toggle as minus) 6007 html += `<i class="fa fa-minus category-toggle" data-parent-id="${cat.categoryId}"></i><a href="javascript:void(0);" class="category-link ${isSelected ? 'selected' : ''}" data-category-id="${cat.categoryId}" data-category-name="${label}">${label}</a>`; 6008 } else { 6009 html += `<span class="category-spacer"></span><a href="javascript:void(0);" class="category-link ${isSelected ? 'selected' : ''}" data-category-id="${cat.categoryId}" data-category-name="${label}">${label}</a>`; 6010 } 6011 6012 if (hasChildren) { 6013 // Filter children to only show matching ones (or all if parent directly matched) 6014 const parentName = (cat.label && cat.label.trim().length > 0 ? cat.label : cat.name).toLowerCase(); 6015 const parentDirectlyMatched = parentName.includes(searchTerm); 6016 6017 // If parent directly matched, show all children. Otherwise, only show matching children. 6018 const childrenToShow = parentDirectlyMatched 6019 ? children 6020 : children.filter(child => matchingChildIds.has(child.categoryId)); 6021 6022 if (childrenToShow.length > 0) { 6023 // Show children expanded when searching 6024 html += `<div id="category-children-${cat.categoryId}" class="category-children" style="display:block;">`; 6025 6026 // Sort children alphabetically 6027 childrenToShow.sort((a, b) => { 6028 const nameA = (a.label && a.label.trim().length > 0 ? a.label : a.name).toLowerCase(); 6029 const nameB = (b.label && b.label.trim().length > 0 ? b.label : b.name).toLowerCase(); 6030 return nameA.localeCompare(nameB); 6031 }); 6032 6033 for (let j = 0; j < childrenToShow.length; j++) { 6034 const child = childrenToShow[j]; 6035 const childLabel = child.label && child.label.trim().length > 0 ? child.label.trim() : child.name; 6036 const childIsSelected = page.terms.categories.id === child.categoryId; 6037 6038 html += `<div class="category-child-item"><a href="javascript:void(0);" class="category-link ${childIsSelected ? 'selected' : ''}" data-category-id="${child.categoryId}" data-category-name="${childLabel}">${childLabel}</a></div>`; 6039 } 6040 6041 html += '</div>'; 6042 } 6043 } 6044 6045 html += '</div>'; 6046 } 6047 6048 // Update both desktop and mobile 6049 $("#FiltersList #CategoryLinkList").html(html); 6050 $("#MobileFilterContent #CategoryLinkList").html(html); 6051 6052 // Hide show more when filtering 6053 $("#FiltersList #CategoryShowMore").hide(); 6054 $("#MobileFilterContent #CategoryArea #CategoryShowMore").hide(); 6055 6056 // Re-attach handlers 6057 $(".category-toggle").off("click").on("click", page.toggleCategoryChildren);
6058 $(".category-link").off("click").on("click", function() { 6059 const categoryId = parseInt($(this).data("category-id")); 6060 const categoryName = $(this).data("category-name"); 6061 $(".category-link").removeClass("selected"); 6062 $(this).addClass("selected"); 6063 page.terms.categories.id = categoryId; 6064 page.terms.categories.label = categoryName; 6065 page.addPill(categoryId, categoryName, "category"); 6066 products.filter.category = page.terms.categories.id; 6067 }); 6068 }, 6069 popularLoaded: function (e) { 6070 $("#PopularList").html(""); 6071 var data = context.get("popular"); 6072 page.popular = data.data; 6073 if (page.popular.length > 0) { 6074 for (var i = 0; i < page.popular.length; i++) { 6075 var element = $("#PopularTemplate").clone(); 6076 element.css("display", "grid"); 6077 element.attr("id", "Product" + i); 6078 element.attr("data-url", "/product/" + page.popular[i].productId + "/" + page.popular[i].name.split(" ").join("-")); 6079 element.html(element.html().split("{{product.url}}").join(products.getSize(200, page.popular[i].url))); 6080 element.html(element.html().split("{{url}}").join(url)); 6081 element.html(element.html().split("{{product.productId}}").join(page.popular[i].productId)); 6082 element.html(element.html().split("{{product.name}}").join(page.popular[i].name)); 6083 element.html(element.html().split("{{product.price}}").join(page.popular[i].cost.toFixed(2))); 6084 $("#PopularList").append(element); 6085 } 6086 $("#PopularArea").show(); 6087 $(".clicker").click(page.goto); 6088 } else { 6089 $("#PopularArea").hide(); 6090 } 6091 } 6092 }; 6093</script>
6093 6094<footer class="footer-1 storefooter"> 6095 <div id="SocialArea"> 6096 <div class="footer-title" style="margin-bottom: 10px; font-weight: 600;">Follow Us, We're Social</div> 6097 <div id="SocialLinks" class="list-inline social-list" style="margin-bottom: 8px;"></div> 6098 </div> 6099 <div class="row"> 6100 <div class="col-sm-12 text-center"> 6101 <p style="margin-bottom: 0px;"><a href="/terms">Terms</a> ⢠<a href="/privacy">Privacy</a><span class="ecomtrue"> ⢠<a href="/returns">Shipping + Returns</a></span></p> 6102 <p style="margin-bottom: 0px;">© Copyright Naperville Running - All Rights Reserved</p> 6103 <p style="margin-bottom: 0px;"><a href="https://www.runfreeproject.com" class="proud box-shadow-wide-link bridaloff" style="margin-top: 10px;" target="_blank"><img alt="Background Image" class="background-image" width="200px" height="47px" style="margin-left: -15px;" src="/assets/images/proud.webp"></a></p> 6104 </div> 6105 </div> 6106 <a class="btn btn-sm back-to-top inner-link" href="#top">Top</a> 6107</footer> 6108<p><meta name="google-site-verification" content="p95OR9FvxmrT4bHAUMAuD5OZ6EvfRse2gyH_JKE3m2E" /></p> 6109 6110</div> 6111<div class="fullLoading"> 6112 <div id="progress"> 6113 <span class="spinner-icon"></span> 6114 </div> 6115 <div class="running"> 6116 <div class="outer"> 6117 <div class="body"> 6118 <div class="arm behind"></div> 6119 <div class="arm front"></div> 6120 <div class="leg behind"></div> 6121 <div class="leg front"></div> 6122 </div> 6123 </div> 6124 </div> 6125 </div> 6126</div> 6127<div class="fullLoadingOrders"> 6128 <div id="progress"> 6129 <span class="spinner-icon"></span> 6130 </div> 6131 <div class="running"> 6132 <div class="outer"> 6133 <div class="body"> 6134 <div class="arm behind"></div> 6135 <div class="arm front"></div> 6136 <div class="leg behind"></div> 6137 <div class="leg front"></div> 6138 </div> 6139 </div> 6140 </div> 6141 <div class="loadMessage"> 6142 <span class="header">Processing, Please be patient</span><br/> 6143 <span style="color: #ff0072; font-weight: 800;"> 6144 Don't Tap Back or Close the Window!<br> 6145 This could cause the order to duplicate or fail.<br> 6146 </span> 6147 <span id="CheckoutStatus"></span> 6148 </div> 6149</div> 6150 6151<!-- 3rd Party Includes -->
6152<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
vendor: 2 bytes, line 6152
6152
6153<script src="/assets/scripts/flexslider.min.js"></script>
6153
6154<script src="/assets/scripts/masonry.min.js"></script>
vendor: 2 bytes, line 6154
6154
6155<script src="/assets/scripts/smooth-scroll.min.js"></script>
vendor: 2 bytes, line 6155
6155
6156<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
vendor: 2 bytes, line 6156
6156
6157<script type="text/javascript" src="/assets/admin/scripts/spectrum.js?v=1.2.0"></script>
6157 6158<!-- Model Objects -->
6159<script type="text/javascript" src="/assets/admin/scripts/models/products.js?v=1.2.0"></script>
vendor: 2 bytes, line 6159
6159
6160<script type="text/javascript" src="/assets/admin/scripts/models/category.js?v=1.2.0"></script>
vendor: 2 bytes, line 6160
6160
6161<script type="text/javascript" src="/assets/admin/scripts/models/brand.js?v=1.2.0"></script>
vendor: 2 bytes, line 6161
6161
6162<script type="text/javascript" src="/assets/scripts/collection.js?v=1.2.0"></script>
vendor: 2 bytes, line 6162
6162
6163<script type="text/javascript" src="/assets/admin/scripts/models/discounts.js?v=1.2.0"></script>
vendor: 2 bytes, line 6163
6163
6164<script type="text/javascript" src="/assets/admin/scripts/models/orders.js?v=1.2.0"></script>
vendor: 2 bytes, line 6164
6164
6165<script type="text/javascript" src="/assets/admin/scripts/models/taxes.js?v=1.2.0"></script>
vendor: 2 bytes, line 6165
6165
6166<script type="text/javascript" src="/assets/admin/scripts/models/states.js?v=1.2.0"></script>
vendor: 2 bytes, line 6166
6166
6167<script type="text/javascript" src="/assets/admin/scripts/models/country.js?v=1.2.0"></script>
vendor: 2 bytes, line 6167
6167
6168<script type="text/javascript" src="/assets/admin/scripts/models/homepage.js?v=1.2.0"></script>
vendor: 2 bytes, line 6168
6168
6169<script type="text/javascript" src="/assets/admin/scripts/models/customers.js?v=1.2.0"></script>
vendor: 2 bytes, line 6169
6169
6170<script type="text/javascript" src="/assets/admin/scripts/models/site.js?v=1.2.0"></script>
vendor: 2 bytes, line 6170
6170
6171<script type="text/javascript" src="/assets/admin/scripts/models/user.js?v=1.2.0"></script>
vendor: 2 bytes, line 6171
6171
6172<script type="text/javascript" src="/assets/admin/scripts/models/address.js?v=1.2.0"></script>
vendor: 2 bytes, line 6172
6172
6173<script type="text/javascript" src="/assets/admin/scripts/models/contents.js?v=1.2.0"></script>
vendor: 2 bytes, line 6173
6173
6174<script type="text/javascript" src="/assets/admin/scripts/models/socials.js?v=1.2.0"></script>
vendor: 2 bytes, line 6174
6174
6175<script type="text/javascript" src="/assets/admin/scripts/models/orders.js?v=1.2.0"></script>
vendor: 2 bytes, line 6175
6175
6176<script type="text/javascript" src="/assets/admin/scripts/models/merchant.js?v=1.2.0"></script>
vendor: 2 bytes, line 6176
6176
6177<script type="text/javascript" src="/assets/admin/scripts/models/store.js?v=1.2.0"></script>
6177
6178<script type="text/javascript" src="/assets/admin/scripts/models/settings.js?v=1.2.0"></script>
6178
6179<script type="text/javascript" src="/assets/scripts/models/wishlist.js?v=1.2.0"></script>
6179 6180<!-- Application Core -->
6181<script type="text/javascript" src="/assets/admin/scripts/context.js?v=1.2.0"></script>
vendor: 2 bytes, line 6181
6181
6182<script type="text/javascript" src="/assets/admin/scripts/growler.js?v=1.2.0"></script>
vendor: 2 bytes, line 6182
6182
6183<script type="text/javascript" src="/assets/admin/scripts/modal.js?v=1.2.0"></script>
vendor: 2 bytes, line 6183
6183
6184<script type="text/javascript" src="/assets/scripts/jquery.datetimepicker.js"></script>
vendor: 2 bytes, line 6184
6184
6185<script type="text/javascript" src="/assets/admin/scripts/service.js?v=1.2.0"></script>
vendor: 2 bytes, line 6185
6185
6186<script type="text/javascript" src="/assets/admin/scripts/cart.js?v=1.2.0"></script>
6186
6187<script type="text/javascript" src="/assets/admin/scripts/store.js?v=1.2.0"></script>
6187
6188<script src="/assets/scripts/scripts.js?v=1.2.0"></script>
6188 6189 6190</body> 6191</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.