1;(function () { 2 'use strict' 3 4 var script = document.currentScript 5 var DEFAULT_APP_URL = 'https://club.dirtstore.co.uk' 6 7 // data-* attributes are author-controlled, but harden the values that flow 8 // into DOM sinks anyway: appUrl/iconUrl become an iframe src, an <img> src and 9 // a window.open target (a javascript:/data: URL there would execute), and 10 // buttonColor is interpolated into a <style> block (a crafted value could 11 // break out and inject CSS rules). 12 function safeHttpUrl(value, fallback) { 13 if (!value) return fallback 14 var v = String(value).trim() 15 // Guard against placeholder strings that slip in when an install snippet 16 // stringifies a null/undefined value, e.g. data-icon-url="null". Without 17 // this, new URL('null', origin) resolves to <origin>/null â a valid https 18 // URL that 404s, so the trigger would show a broken <img> instead of 19 // falling back to the built-in icon. 20 if (v === '' || v === 'null' || v === 'undefined') return fallback 21 try { 22 var u = new URL(v, window.location.href) 23 // Returning u.href (not the raw value) normalises + percent-encodes, 24 // so quotes can't break out of the attribute we concatenate it into. 25 if (u.protocol === 'http:' || u.protocol === 'https:') return u.href 26 } catch (e) { 27 /* fall through */ 28 } 29 return fallback 30 } 31 function safeColor(value, fallback) { 32 if (value && /^(#[0-9a-fA-F]{3,8}|rgba?\([\d.,\s%]+\)|[a-zA-Z]{1,30})$/.test(value)) return value 33 return fallback 34 } 35 36 var appUrl = safeHttpUrl(script && script.getAttribute('data-app-url'), DEFAULT_APP_URL).replace(/\/+$/, '') 37 var position = (script && script.getAttribute('data-position')) || 'bottom-right' 38 var buttonColor = safeColor(script && script.getAttribute('data-color'), '#ff0000') 39 var iconChoice = (script && script.getAttribute('data-icon')) || 'star' 40 var iconUrl = safeHttpUrl(script && script.getAttribute('data-icon-url'), null) 41 var iconSvg = script && script.getAttribute('data-icon-svg') 42 43 if (window.Shopify && window.Shopify.designMode) return 44 45 if (document.getElementById('cds-embed-root')) return 46 47 var css = document.createElement('style') 48 css.id = 'cds-embed-styles' 49 css.textContent = [ 50 '#cds-trigger {', 51 ' all: unset;', 52 ' position: fixed;', 53 ' z-index: 2147483646;', 54 ' width: 56px;', 55 ' height: 56px;', 56 ' border-radius: 50%;', 57 ' cursor: pointer;', 58 ' display: flex;', 59 ' align-items: center;', 60 ' justify-content: center;', 61 ' color: #fff;', 62 ' background: ' + buttonColor + ';', 63 ' box-shadow: 0 4px 20px rgba(0,0,0,0.3);', 64 ' transition: transform 0.25s cubic-bezier(0.4,0,0.2,1), box-shadow 0.25s ease;', 65 position === 'bottom-left' ? ' bottom: 20px; left: 20px;' : ' bottom: 20px; right: 20px;', 66 '}', 67 '#cds-trigger:hover {', 68 ' transform: scale(1.08);', 69 ' box-shadow: 0 6px 28px rgba(0,0,0,0.4);', 70 '}', 71 '#cds-trigger:active { transform: scale(0.95); }', 72 '#cds-trigger.cds-is-open { transform: rotate(90deg); }', 73 '#cds-trigger.cds-is-open:hover { transform: rotate(90deg) scale(1.08); }', 74 '#cds-trigger svg {', 75 ' width: 26px;', 76 ' height: 26px;', 77 ' fill: #fff;', 78 '}', 79 '#cds-trigger img {', 80 ' width: 28px;', 81 ' height: 28px;', 82 ' border-radius: 6px;', 83 ' object-fit: contain;', 84 '}', 85 86 '#cds-panel {', 87 ' all: unset;', 88 ' position: fixed;', 89 ' z-index: 2147483647;', 90 ' display: flex;', 91 ' flex-direction: column;', 92 ' background: #0b0b0b;', 93 ' border-radius: 20px;', 94 ' overflow: hidden;', 95 ' box-shadow: 0 12px 60px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.08);', 96 ' opacity: 0;', 97 ' transform: translateY(16px) scale(0.97);', 98 ' transition: opacity 0.2s ease, transform 0.3s cubic-bezier(0.4,0,0.2,1);', 99 ' pointer-events: none;', 100 position === 'bottom-left' 101 ? ' transform-origin: bottom left; bottom: 88px; left: 20px;' 102 : ' transform-origin: bottom right; bottom: 88px; right: 20px;', 103 ' width: 400px;', 104 ' height: 680px;', 105 ' max-height: calc(100vh - 108px);', 106 '}', 107 '#cds-panel.cds-open {', 108 ' opacity: 1;', 109 ' transform: translateY(0) scale(1);', 110 ' pointer-events: auto;', 111 '}', 112 113 '#cds-panel .cds-header {', 114 ' all: unset;', 115 ' display: flex;', 116 ' align-items: center;', 117 ' justify-content: space-between;', 118 ' padding: 12px 16px;', 119 ' border-bottom: 1px solid rgba(255,255,255,0.08);', 120 ' background: #141414;', 121 ' flex-shrink: 0;', 122 '}', 123 '#cds-panel .cds-header-title {', 124 ' all: unset;', 125 ' display: flex;', 126 ' align-items: center;', 127 ' gap: 10px;', 128 ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;', 129 ' font-size: 14px;', 130 ' font-weight: 600;', 131 ' color: #f5f5f5;', 132 '}', 133 '#cds-panel .cds-header-title img { width: 24px; height: 24px; border-radius: 6px; }', 134 '#cds-panel .cds-header-actions { all: unset; display: flex; gap: 4px; }', 135 '#cds-panel .cds-header-btn {', 136 ' all: unset;', 137 ' width: 32px;', 138 ' height: 32px;', 139 ' border-radius: 8px;', 140 ' cursor: pointer;', 141 ' display: flex;', 142 ' align-items: center;', 143 ' justify-content: center;', 144 ' color: #fff;', 145 ' transition: background 0.15s, color 0.15s;', 146 '}', 147 '#cds-panel .cds-header-btn:hover { background: rgba(255,255,255,0.08); color: #f5f5f5; }', 148 '#cds-panel .cds-header-btn svg { width: 16px; height: 16px; fill: currentColor; }', 149 150 '#cds-panel .cds-iframe-wrapper {', 151 ' flex: 1;', 152 ' display: flex;', 153 ' flex-direction: column;', 154 ' min-height: 0;', 155 '}', 156 '#cds-panel .cds-iframe {', 157 ' all: unset;', 158 ' flex: 1;', 159 ' display: block;', 160 ' width: 100%;', 161 ' border: none;', 162 ' background: #0b0b0b;', 163 ' -webkit-overflow-scrolling: touch;', 164 ' overscroll-behavior: contain;', 165 '}', 166 167 '#cds-backdrop {', 168 ' display: none;', 169 ' position: fixed;', 170 ' inset: 0;', 171 ' z-index: 2147483645;', 172 ' background: rgba(0,0,0,0.5);', 173 ' backdrop-filter: blur(4px);', 174 ' -webkit-backdrop-filter: blur(4px);', 175 ' opacity: 0;', 176 ' transition: opacity 0.25s ease;', 177 ' pointer-events: none;', 178 '}', 179 '#cds-backdrop.cds-open {', 180 ' opacity: 1;', 181 ' pointer-events: auto;', 182 '}', 183 184 'html.cds-scroll-lock {', 185 ' overflow: hidden !important;', 186 '}', 187 'body.cds-scroll-lock {', 188 ' position: fixed;', 189 ' overflow: hidden !important;', 190 ' width: 100%;',
191 ' overscroll-behavior: none;', 192 '}', 193 194 '@media (max-width: 480px) {', 195 ' #cds-panel {', 196 ' inset: 0 !important;', 197 ' width: 100% !important;', 198 ' height: 100% !important;', 199 ' max-height: 100% !important;', 200 ' border-radius: 0 !important;', 201 ' }', 202 ' #cds-trigger { width: 50px; height: 50px; bottom: 16px; right: 16px; }', 203 ' #cds-trigger svg { width: 22px; height: 22px; }', 204 '}' 205 ].join('\n') 206 document.head.appendChild(css) 207 208 var icons = { 209 star: '<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path d="M234.5,114.38l-45.1,39.36,13.51,58.6a16,16,0,0,1-23.84,17.34l-51.11-31-51,31a16,16,0,0,1-23.84-17.34L66.61,153.8,21.5,114.38a16,16,0,0,1,9.11-28.06l59.46-5.15,23.21-55.36a15.95,15.95,0,0,1,29.44,0h0L166,81.17l59.44,5.15a16,16,0,0,1,9.11,28.06Z"/></svg>', 210 gift: '<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path d="M216,72H180.92c.39-.33.79-.65,1.17-1A29.53,29.53,0,0,0,192,49.57,32.62,32.62,0,0,0,158.44,16,29.53,29.53,0,0,0,137,25.91a54.94,54.94,0,0,0-9,14.48,54.94,54.94,0,0,0-9-14.48A29.53,29.53,0,0,0,97.56,16,32.62,32.62,0,0,0,64,49.57,29.53,29.53,0,0,0,73.91,71c.38.33.78.65,1.17,1H40A16,16,0,0,0,24,88v32a16,16,0,0,0,16,16v64a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V136a16,16,0,0,0,16-16V88A16,16,0,0,0,216,72ZM149,36.51a13.69,13.69,0,0,1,10-4.5h.49A16.62,16.62,0,0,1,176,49.08a13.69,13.69,0,0,1-4.5,10c-9.49,8.4-25.24,11.36-35,12.4C137.7,60.89,141,45.5,149,36.51ZM80,49.08A16.62,16.62,0,0,1,96.56,32h.49a13.69,13.69,0,0,1,10,4.5c8.05,9,11.35,24.38,12.56,35.09-9.76-1-25.51-4-35-12.4A13.69,13.69,0,0,1,80,49.08ZM40,88H120v32H40Zm16,48h64v64H56Zm144,64H136V136h64Zm16-80H136V88h80Z"/></svg>', 211 coin: '<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm0-144a56,56,0,1,0,56,56A56.06,56.06,0,0,0,128,72Zm0,96a40,40,0,1,1,40-40A40,40,0,0,1,128,168Z"/></svg>', 212 trophy: '<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path d="M232,64H208V48a8,8,0,0,0-8-8H56a8,8,0,0,0-8,8V64H24A16,16,0,0,0,8,80v32a56.06,56.06,0,0,0,56,56h1.11A56.06,56.06,0,0,0,112,206.41V224H88a8,8,0,0,0,0,16h80a8,8,0,0,0,0-16H144V206.41A56.06,56.06,0,0,0,190.89,168H192a56.06,56.06,0,0,0,56-56V80A16,16,0,0,0,232,64ZM64,152a40,40,0,0,1-40-40V80H48v40a56.06,56.06,0,0,0,24,45.94A40.18,40.18,0,0,1,64,152Zm128,0a40.18,40.18,0,0,1-8,13.94A56.06,56.06,0,0,0,208,120V80h24v32A40,40,0,0,1,192,152Z"/></svg>', 213 tag: '<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path d="M243.31,136,144,36.69A15.86,15.86,0,0,0,132.69,32H40a8,8,0,0,0-8,8v92.69A15.86,15.86,0,0,0,36.69,144L136,243.31a16,16,0,0,0,22.63,0l84.68-84.68a16,16,0,0,0,0-22.63ZM84,96A12,12,0,1,1,96,84,12,12,0,0,1,84,96Z"/></svg>', 214 heart: '<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path d="M240,94c0,70-103.79,126.66-108.21,129a8,8,0,0,1-7.58,0C119.79,220.66,16,164,16,94A62.07,62.07,0,0,1,78,32c20.65,0,38.73,8.88,50,23.89C139.27,40.88,157.35,32,178,32A62.07,62.07,0,0,1,240,94Z"/></svg>', 215 percent: 216 '<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path d="M205.66,61.64a8,8,0,0,1,0,11.32l-128,128a8,8,0,0,1-11.32-11.32l128-128A8,8,0,0,1,205.66,61.64ZM50.54,101.44a36,36,0,1,1,50.92,0A36,36,0,0,1,50.54,101.44Zm11.31-39.61A20,20,0,1,0,90.15,90.15,20,20,0,0,0,61.85,61.83Zm132.71,92.73a36,36,0,1,1-50.92,0A36,36,0,0,1,194.56,154.56Zm-11.31,39.61a20,20,0,1,0-28.3-28.32,20,20,0,0,0,28.3,28.32Z"/></svg>', 217 sparkle: 218 '<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path d="M208,144a15.78,15.78,0,0,1-10.42,14.94l-51.65,19-19,51.61a15.92,15.92,0,0,1-29.88,0L78,177.91l-51.62-19a15.92,15.92,0,0,1,0-29.88l51.65-19,19-51.61a15.92,15.92,0,0,1,29.88,0l19,51.65,51.61,19A15.78,15.78,0,0,1,208,144ZM152,48h16V64a8,8,0,0,0,16,0V48h16a8,8,0,0,0,0-16H184V16a8,8,0,0,0-16,0V32H152a8,8,0,0,0,0,16Zm88,48H232V88a8,8,0,0,0-16,0v8H208a8,8,0,0,0,0,16h8v8a8,8,0,0,0,16,0v-8h8a8,8,0,0,0,0-16Z"/></svg>' 219 } 220 221 var triggerIcon 222 if (iconSvg) { 223 triggerIcon = iconSvg 224 } else if (iconUrl) { 225 triggerIcon = 226 '<img src="' + iconUrl + '" alt="" style="width:28px;height:28px;border-radius:6px;object-fit:contain;" />' 227 } else { 228 triggerIcon = icons[iconChoice] || icons.star 229 } 230 231 var closeIcon = 232 '<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z" fill="currentColor"/></svg>' 233 var expandIcon = 234 '<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path d="M224,104a8,8,0,0,1-16,0V59.32l-66.33,66.34a8,8,0,0,1-11.32-11.32L196.68,48H152a8,8,0,0,1,0-16h64a8,8,0,0,1,8,8Zm-40,24a8,8,0,0,0-8,8v72H48V80h72a8,8,0,0,0,0-16H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V136A8,8,0,0,0,184,128Z" fill="currentColor"/></svg>' 235 236 var root = document.createElement('div') 237 root.id = 'cds-embed-root' 238 root.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;overflow:visible;z-index:2147483645;' 239 240 var backdrop = document.createElement('div') 241 backdrop.id = 'cds-backdrop' 242 root.appendChild(backdrop) 243 244 var panel = document.createElement('div')
245 panel.id = 'cds-panel' 246 panel.style.visibility = 'hidden' 247 panel.innerHTML = [ 248 '<div class="cds-header">', 249 ' <div class="cds-header-title">', 250 // /logo-mark.png, not /cds.png: the latter is a 4995x4995 (~900KB) asset and 251 // this renders it at 24px on every storefront page that loads the embed. 252 ' <img src="' + appUrl + '/logo-mark.png" alt="" width="24" height="24" />', 253 ' <span>Club Dirt Store</span>', 254 ' </div>', 255 ' <div class="cds-header-actions">', 256 ' <button class="cds-header-btn cds-expand-btn" title="Open in new tab">' + expandIcon + '</button>', 257 ' <button class="cds-header-btn cds-close-btn" title="Close">' + closeIcon + '</button>', 258 ' </div>', 259 '</div>' 260 ].join('') 261 262 var iframe = document.createElement('iframe') 263 iframe.className = 'cds-iframe' 264 iframe.setAttribute('allow', 'clipboard-write') 265 iframe.setAttribute('loading', 'lazy') 266 iframe.setAttribute('scrolling', 'yes') 267 var iframeWrapper = document.createElement('div') 268 iframeWrapper.className = 'cds-iframe-wrapper' 269 iframeWrapper.appendChild(iframe) 270 panel.appendChild(iframeWrapper) 271 root.appendChild(panel) 272 273 panel.querySelector('.cds-close-btn').addEventListener('click', doClose) 274 panel.querySelector('.cds-expand-btn').addEventListener('click', function () { 275 window.open(appUrl + '/widget', '_blank') 276 doClose() 277 }) 278 backdrop.addEventListener('click', doClose) 279 backdrop.addEventListener( 280 'touchmove', 281 function (e) { 282 if (isOpen) e.preventDefault() 283 }, 284 { passive: false } 285 ) 286 287 var trigger = document.createElement('button') 288 trigger.id = 'cds-trigger' 289 trigger.setAttribute('aria-label', 'Open Club Dirt Store rewards') 290 trigger.innerHTML = triggerIcon 291 trigger.addEventListener('click', toggle) 292 root.appendChild(trigger) 293 294 document.body.appendChild(root) 295 296 var isOpen = false 297 var _bodyScrollX = 0 298 var _bodyScrollY = 0 299 var _scrollLocked = false 300 301 function isMobile() { 302 return window.innerWidth <= 480 303 } 304 305 function lockScroll() { 306 if (_scrollLocked) return 307 _scrollLocked = true 308 _bodyScrollX = window.pageXOffset 309 _bodyScrollY = window.pageYOffset 310 document.body.style.top = '-' + _bodyScrollY + 'px' 311 document.body.classList.add('cds-scroll-lock') 312 document.documentElement.classList.add('cds-scroll-lock') 313 } 314 315 function unlockScroll() { 316 if (!_scrollLocked) return 317 _scrollLocked = false 318 document.body.classList.remove('cds-scroll-lock') 319 document.documentElement.classList.remove('cds-scroll-lock') 320 document.body.style.top = '' 321 window.scrollTo(_bodyScrollX, _bodyScrollY) 322 } 323 324 function doOpen() { 325 if (isOpen) return 326 isOpen = true 327 panel.style.visibility = 'visible' 328 if (isMobile()) { 329 lockScroll() 330 backdrop.style.display = 'block' 331 } 332 if (!iframe.src) { 333 iframe.src = appUrl + '/widget?embed=1' 334 } 335 void panel.offsetHeight 336 panel.classList.add('cds-open') 337 backdrop.classList.add('cds-open') 338 trigger.classList.add('cds-is-open') 339 trigger.innerHTML = closeIcon 340 } 341 342 function doClose() { 343 if (!isOpen) return 344 isOpen = false 345 unlockScroll() 346 panel.classList.remove('cds-open') 347 backdrop.classList.remove('cds-open') 348 trigger.classList.remove('cds-is-open') 349 trigger.innerHTML = triggerIcon 350 trigger.focus({ preventScroll: true }) 351 setTimeout(function () { 352 if (!isOpen) { 353 panel.style.visibility = 'hidden' 354 backdrop.style.display = '' 355 } 356 }, 350) 357 } 358 359 function toggle() { 360 isOpen ? doClose() : doOpen() 361 } 362 363 window.addEventListener('resize', function () { 364 if (_scrollLocked && !isMobile()) unlockScroll() 365 if (isOpen && isMobile() && !_scrollLocked) lockScroll() 366 }) 367 368 document.addEventListener('keydown', function (e) { 369 if (e.key === 'Escape' && isOpen) doClose() 370 }) 371 372 // The widget (inside the iframe) can ask us to take the customer to a store 373 // page, e.g. "My Account". Only trust messages from our own iframe origin, and 374 // only follow http(s) URLs, before closing the panel and navigating the host. 375 var appOrigin 376 try { 377 appOrigin = new URL(appUrl).origin 378 } catch (e) { 379 appOrigin = null 380 } 381 window.addEventListener('message', function (e) { 382 if (!appOrigin || e.origin !== appOrigin) return 383 var data = e.data 384 if (!data || data.type !== 'cds:navigate' || typeof data.url !== 'string') return 385 var target 386 try { 387 target = new URL(data.url) 388 } catch (err) { 389 return 390 } 391 if (target.protocol !== 'http:' && target.protocol !== 'https:') return 392 doClose() 393 window.location.href = target.href 394 }) 395 396 window.ClubDirtStore = { 397 open: doOpen, 398 close: doClose, 399 toggle: toggle, 400 ready: true, 401 appUrl: appUrl 402 } 403 window.dispatchEvent(new CustomEvent('cds:ready')) 404})()
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.