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