1import { messageFromGameFrame } from 'wild/game_frame'; 2 3// Drag handler functions 4function closeMenu(menuType) { 5 const config = { 6 mobile: { 7 menuSelector: '.navbar', 8 activeClass: 'active-mobile', 9 buttonSelector: '#mobile-menu-link', 10 removeOverflow: true 11 }, 12 social: { 13 menuSelector: '.social-menu-navbar', 14 activeClass: 'active', 15 buttonSelector: '#social-menu-modal-link', 16 removeOverflow: false, 17 hasChat: true 18 } 19 }; 20 21 const cfg = config[menuType]; 22 if (!cfg) return; 23 24 const menu = document.querySelector(cfg.menuSelector); 25 if (!menu) return; 26 27 // Get current transform value for smooth animation from current position 28 const currentTransform = menu.style.transform; 29 const currentY = currentTransform ? parseInt(currentTransform.match(/translateY\((-?\d+)px\)/)?.[1]) || 0 : 0; 30 31 // Enable transition for smooth animation 32 menu.style.transition = 'transform 0.3s ease-in-out'; 33 34 // Animate to fully closed position from current position 35 menu.style.transform = `translateY(${menu.offsetHeight + Math.max(currentY, 0)}px)`; 36 37 setTimeout(() => { 38 // Remove active class and button state 39 menu.classList.remove(cfg.activeClass); 40 document.querySelector(cfg.buttonSelector)?.classList.remove('active'); 41 42 // Handle overflow for mobile menu 43 if (cfg.removeOverflow) { 44 document.body.classList.remove('overflow-hidden'); 45 } 46 47 // Handle chat for social menu 48 if (cfg.hasChat) { 49 const chatContainer = menu.querySelector('.social-chat-container'); 50 if (chatContainer && chatContainer.classList.contains('active')) { 51 chatContainer.classList.remove('active'); 52 menu.classList.remove('chat-active'); 53 } 54 } 55 56 // Clear manual styles 57 menu.style.transform = ''; 58 menu.style.transition = ''; 59 }, 300); 60} 61 62function setupDragHandler(menuElement, pullerElement, closeCallback, preventDragCondition = null) { 63 if (!menuElement || !pullerElement) return; 64 65 let startY, isDragging = false; 66 67 const handleTouchStart = (e) => { 68 // Check prevent condition (e.g., chat-active for social menu) 69 if (preventDragCondition && preventDragCondition()) return; 70 71 startY = e.touches[0].clientY; 72 isDragging = true; 73 // Disable transition during drag 74 menuElement.style.transition = 'none'; 75 }; 76 77 const handleTouchMove = (e) => { 78 if (!isDragging) return; 79 80 const currentY = e.touches[0].clientY; 81 const deltaY = currentY - startY; 82 83 if (deltaY > 0) { 84 menuElement.style.transform = `translateY(${deltaY}px)`; 85 } 86 }; 87 88 const handleTouchEnd = (e) => { 89 if (!isDragging) return; 90 91 isDragging = false; 92 const deltaY = e.changedTouches[0].clientY - startY; 93 94 // Re-enable transition for smooth animation 95 menuElement.style.transition = 'transform 0.3s ease-in-out'; 96 97 if (deltaY > 100) { 98 // Use provided close callback 99 if (typeof closeCallback === 'function') { 100 closeCallback(); 101 } 102 } else { 103 // Snap back to original position 104 menuElement.style.transform = 'translateY(0)'; 105 setTimeout(() => { 106 menuElement.style.transition = ''; 107 }, 300); 108 } 109 }; 110 111 pullerElement.addEventListener('touchstart', handleTouchStart, { passive: true }); 112 pullerElement.addEventListener('touchmove', handleTouchMove, { passive: true }); 113 pullerElement.addEventListener('touchend', handleTouchEnd); 114} 115 116// Games run cross-origin and post `{ type: 'open-cashier' }` to reach the cashier. The source 117// check matters: any embed on the page (chat, KYC, tag manager) can post a message too. 118window.addEventListener('message', (event) => { 119 const data = event.data; 120 const type = typeof data === 'string' ? data : data?.type || data?.action; 121 122 if (type !== 'open-cashier' || !messageFromGameFrame(event.source)) return; 123 124 const detail = {}; 125 const amount = data?.amount; 126 const packageId = data?.packageId ?? data?.package_id; 127 128 if (amount !== undefined) detail.amount = amount; 129 if (packageId !== undefined) detail.packageId = packageId; 130 // `pair` is threaded through for the Fluid cashier only; Coinflow ignores it (Phase-1 USD). 131 if (data?.pair !== undefined) detail.pair = data.pair; 132 133 // Coinflow-first, mirroring the click bridge below: prefer the Coinflow cashier when mounted, 134 // fall back to Fluid, otherwise the player has no cashier mounted. 135 if (document.querySelector('[data-controller="coinflow-cashier"]')) { 136 window.dispatchEvent(new CustomEvent('open-coinflow-cashier', { detail })); 137 return; 138 } 139 140 if (document.getElementById('fluid-widget-global')) { 141 window.dispatchEvent(new CustomEvent('open-fluid-cashier', { detail })); 142 return; 143 } 144 145 console.warn('open-cashier ignored: cashier is not mounted for this player'); 146}); 147 148document.addEventListener("turbo:load", function (event) {
149 let csrfToken = document.querySelector("meta[name='csrf-token']").content 150 document.querySelectorAll("form input[name='authenticity_token']").forEach(function (el) { 151 el.value = csrfToken 152 }) 153 }); 154 155document.addEventListener("turbo:morph", function (event) { 156 let csrfToken = document.querySelector("meta[name='csrf-token']").content 157 document.querySelectorAll("form input[name='authenticity_token']").forEach(function (el) { 158 el.value = csrfToken 159 }) 160}); 161 162// XtremePush injects popups and notification banners at <body> level; a Turbo snapshot 163// of them comes back as inert ghost copies on restore visits. 164document.addEventListener("turbo:before-cache", function () { 165 document 166 .querySelectorAll(".webpush-swal2-container, #webpush-local-notification-stack") 167 .forEach(function (el) { el.remove() }); 168 [document.documentElement, document.body].forEach(function (el) { 169 el.classList.remove("webpush-swal2-shown", "webpush-swal2-iosfix", "webpush-swal2-toast-shown") 170 }); 171}); 172 173$(document).on("turbo:before-fetch-response", (event) => { 174 const fetchResponse = event.detail.fetchResponse; 175 let redirectUrl = fetchResponse.header("Turbo-Visit"); 176 if (!redirectUrl) { 177 if (fetchResponse.redirected === true) { 178 redirectUrl = fetchResponse.location.href; 179 } 180 } 181 182 if (redirectUrl) { 183 // If user was logged in, verify session is still active before following redirect 184 const wasLoggedIn = !!document.querySelector('meta[name="player-id"]') 185 if (wasLoggedIn) { 186 event.preventDefault() 187 fetchResponse.response.clone().text().then(html => { 188 if (!html.includes('name="player-id"')) { 189 if (window.Turbo) Turbo.cache.clear() 190 window.location.reload() 191 } else { 192 if (typeof $.fancybox !== 'undefined') $.fancybox.close() 193 window.location.href = redirectUrl 194 } 195 }) 196 return 197 } 198 199 if (typeof $.fancybox !== 'undefined') { 200 $.fancybox.close(); 201 } 202 203 window.location.href = redirectUrl; 204 } 205}); 206 207$(document).ready(function () { 208}); 209 210window.listenersInitialized = false 211 212// The rakeback pop-up's URL anchor is brand-specific: Vegas Builder and SlotHub ship the feature 213// as "Rakeback" and must not surface Coinz's "coinzback" in the address bar; other wild brands keep it. 214const RAKEBACK_BRANDS = ['vegasbuilder', 'farmspins', 'slothub']; 215const RAKEBACK_HASH = RAKEBACK_BRANDS.includes(document.body?.dataset?.subBrandName) ? 'rakeback' : 'coinzback'; 216
217const MODAL_HASH_MAP = { 218 'dailyquest': 'dailyChallengeModal', 219 'races': 'leaderboardModal', 220 'refer': 'referralStatsModal', 221 'bonus_code': 'bonusCodeModal', 222 [RAKEBACK_HASH]: 'rakebackModal' 223}; 224 225// Game iframes can post arbitrary messages â trust only our own origin and *.shuftipro.com. 226function trustedKycMessageOrigin(origin) { 227 if (origin === window.location.origin) return true; 228 229 try { 230 const host = new URL(origin).hostname; 231 return host === 'shuftipro.com' || host.endsWith('.shuftipro.com'); 232 } catch { 233 return false; 234 } 235} 236 237// The reloaded verification tab can re-post the same terminal event while the webhook lags â act on each once. 238function firstKycStatusHandling(status) { 239 const key = `kycStatusHandledAt:${status}`; 240 const handledAt = parseInt(sessionStorage.getItem(key), 10); 241 if (!isNaN(handledAt) && Date.now() - handledAt < 90000) return false; 242 243 sessionStorage.setItem(key, String(Date.now())); 244 return true; 245} 246 247// Every Swiper built by initializeSliders, so the ones Turbo detaches can be torn down. 248window.wildSwiperRegistry = window.wildSwiperRegistry || []; 249 250function trackSwiper(swiper) { 251 window.wildSwiperRegistry.push(swiper); 252 return swiper; 253} 254 255function releaseDetachedSwipers() { 256 window.wildSwiperRegistry = window.wildSwiperRegistry.filter(function (swiper) { 257 if (swiper.el && swiper.el.isConnected) return true; 258 259 try { 260 swiper.destroy(true, false); 261 } catch (e) { 262 // already torn down â nothing to release 263 } 264 return false; 265 }); 266} 267 268$(document).on("ready turbo:load", function() { 269 function initializeSliders() { 270 // Turbo replaces #games (and #lobby-carousels) wholesale on every client-side hop, so the 271 // element a Swiper was built on gets detached and a fresh one takes its place. Swiper's 272 // observeParents puts a MutationObserver on <body>, which keeps the old instance â and its 273 // autoplay rAF loop â alive forever. Nothing else destroys them, so do it here. 274 releaseDetachedSwipers(); 275 276 // Touch devices scroll the horizontal rows natively (Swiper cssMode) instead of via JS 277 // transforms â see the per-slider configs below. Desktop keeps transform mode. 278 const useNativeScroll = window.matchMedia('(pointer: coarse)').matches; 279 // CORE-454 (behind lobby_dynamic_refresh via the body.lobby-refresh class from the layout): 280 // whole-tile rails (6 desktop / 4 mobile), one-view chevron paging, mobile chevrons in sync. 281 const lobbyRefresh = document.body.classList.contains('lobby-refresh'); 282 283 $(".slots__slider").each(function (index) { 284 const $sliderContainer = $(this); 285 const $sliderParent = $sliderContainer.closest(".slots__items"); 286 287 // "Game suggestions" (search modal) is a native horizontal scroller, not a 288 // Swiper â it lives in a display:none fancybox modal, so Swiper here measured a 289 // 0-width container and every arrow/swipe moved a pixel. See games_search.js. 290 if (this.id === 'slider-search-recommended') return; 291 292 // Check if Swiper is already initialized on this element 293 if (this.swiper) { 294 console.warn('Swiper already initialized on slots slider, skipping') 295 return 296 } 297 298 // "Today's Matches" carousel needs wide composite VS-pair slides; 299 // let the slide's CSS width win instead of dividing the container into 5.5. 300 const isVsPair = $sliderContainer.closest('.vs-pair-section').length > 0; 301 302 // Touch: scroll every game row natively instead of via Swiper cssMode, which is 303 // flaky on iOS Safari (arrows/swipe barely move â a pixel â on every row). Slide 304 // widths come from the .slots__slider:not(.swiper-initialized) pre-init CSS, so we 305 // just flip overflow to scroll and drive the edge-blur + arrows ourselves. Desktop 306 // keeps Swiper below (JS transform) for the hover lift. VS-pair rows keep Swiper â
307 // their fixed CSS slide width would be overridden by the pre-init rule in native mode. 308 if (useNativeScroll && !isVsPair) { 309 if (this._nativeSlider) return; 310 this._nativeSlider = true; 311 312 const scroller = this; 313 scroller.style.overflowX = 'auto'; 314 scroller.style.overflowY = 'hidden'; 315 // Allow BOTH axes: horizontal pans the carousel, vertical falls through to 316 // the page (the row has overflow-y:hidden). Bare pan-x blocks page scroll; 317 // auto lets iOS's page scroll steal the horizontal gesture. 318 scroller.style.touchAction = 'pan-x pan-y'; 319 scroller.style.webkitOverflowScrolling = 'touch'; 320 scroller.style.scrollBehavior = 'smooth'; 321 322 if (lobbyRefresh) { 323 // Mirror the provider carousel: dim + disable a chevron at the scroll extreme 324 // (kept in sync with swipe), and advance one full view (4 tiles == clientWidth). 325 const prevBtn = $sliderParent.find('.swiper-button-prev')[0]; 326 const nextBtn = $sliderParent.find('.swiper-button-next')[0]; 327 const updateNativeNav = function () { 328 const atStart = scroller.scrollLeft <= 1; 329 const atEnd = scroller.scrollLeft + scroller.clientWidth >= scroller.scrollWidth - 1; 330 if (prevBtn) prevBtn.toggleAttribute('disabled', atStart); 331 if (nextBtn) nextBtn.toggleAttribute('disabled', atEnd); 332 }; 333 scroller.addEventListener('scroll', updateNativeNav, { passive: true }); 334 // Recompute after layout/images settle â a single rAF can run before the rail has 335 // its final scrollWidth, which would otherwise leave a chevron dimmed at start. 336 requestAnimationFrame(updateNativeNav); 337 setTimeout(updateNativeNav, 300); 338 const bindRefreshArrow = function (btn, direction) { 339 if (!btn) return; 340 btn.addEventListener('click', function (e) { 341 e.preventDefault(); 342 scroller.scrollBy({ left: direction * scroller.clientWidth, behavior: 'smooth' }); 343 }); 344 }; 345 bindRefreshArrow(nextBtn, 1); 346 bindRefreshArrow(prevBtn, -1); 347 return; 348 } 349 350 const updateNativeBlur = function () { 351 scroller.classList.toggle('is-blur-left', scroller.scrollLeft > 2); 352 scroller.classList.toggle( 353 'is-blur-no-right', 354 scroller.scrollLeft + scroller.clientWidth >= scroller.scrollWidth - 2 355 ); 356 }; 357 scroller.addEventListener('scroll', updateNativeBlur, { passive: true }); 358 updateNativeBlur(); 359 360 const stepBy = function () { 361 const slide = scroller.querySelector('.swiper-slide'); 362 return slide ? slide.getBoundingClientRect().width + 12 : Math.round(scroller.clientWidth * 0.8); 363 }; 364 const bindArrow = function (btn, direction) { 365 if (!btn) return; 366 btn.addEventListener('click', function (e) { 367 e.preventDefault(); 368 scroller.scrollBy({ left: direction * stepBy(), behavior: 'smooth' }); 369 }); 370 }; 371 bindArrow($sliderParent.find('.swiper-button-next')[0], 1); 372 bindArrow($sliderParent.find('.swiper-button-prev')[0], -1); 373 return; 374 } 375 376 // Edge blur follows scroll position: left fade off at the start, right off at the end. 377 const updateBlur = function (swiper) { 378 const el = swiper.el; 379 el.classList.toggle("is-blur-left", !swiper.isBeginning); 380 el.classList.toggle("is-blur-no-right", swiper.isEnd); 381 }; 382 383 const slider = trackSwiper(new Swiper(this, { 384 // On touch devices scroll natively instead of via JS transforms: iOS Safari won't 385 // let a bounded transform slider swipe back once it hits the end. Native overflow 386 // scroll keeps a real start/end yet scrolls both ways (fixes the stuck-at-end bug). 387 cssMode: useNativeScroll, 388 observer: true, 389 observeParents: true, 390 spaceBetween: isVsPair ? 16 : 12, 391 // CORE-454 (lobbyRefresh): whole tiles matching the All grid (6 desktop / 4 mobile),
392 // chevron advances one full view (slidesPerGroup == view). Off: the original 5.5/4.1. 393 slidesPerView: isVsPair ? 'auto' : (lobbyRefresh ? 6 : 5.5), 394 slidesPerGroup: isVsPair ? 1 : (lobbyRefresh ? 6 : 1), 395 navigation: { 396 nextEl: $sliderParent.find(".swiper-button-next")[0], 397 prevEl: $sliderParent.find(".swiper-button-prev")[0], 398 }, 399 wrapperClass: "swiper-wrapper", 400 slideClass: "swiper-slide", 401 breakpoints: isVsPair ? {} : (lobbyRefresh ? { 402 320: { 403 slidesPerView: 4, 404 slidesPerGroup: 4, 405 spaceBetween: 8, 406 }, 407 768: { 408 slidesPerView: 6, 409 slidesPerGroup: 6, 410 }, 411 } : { 412 320: { 413 slidesPerView: 4.1, 414 spaceBetween: 8, 415 }, 416 767: { 417 slidesPerView: 5.5, 418 }, 419 991: { 420 slidesPerView: 5.5, 421 }, 422 1260: { 423 slidesPerView: 5.5, 424 }, 425 }), 426 on: { 427 init: updateBlur, 428 setTranslate: updateBlur, 429 resize: updateBlur, 430 observerUpdate: updateBlur, 431 }, 432 })); 433 434 $sliderContainer.data("swiper", slider); 435 }); 436 437 // Recent Wins uses a pure-CSS marquee instead of Swiper. 438 // Swiper's freeMode + loop + autoplay doesn't pause/resume 439 // cleanly on hover; a CSS keyframe animation with 440 // animation-play-state gives us reliable pause-on-hover for 441 // free. Cards are cloned once so the -50% loop wraps seamlessly. 442 // Available Packs row â Swiper with shared swiper__nav buttons, 443 // so it matches the look of every other carousel on the page. 444 $(".lobby-coins__scroll.swiper").each(function () { 445 if (this.swiper) { 446 console.warn('Swiper already initialized on lobby-coins, skipping'); 447 return; 448 } 449 const $section = $(this).closest(".lobby-coins"); 450 451 // On touch, scroll the packs strip natively instead of via Swiper. The 452 // cards have fixed CSS widths and the inline flex guard already lays them 453 // out, so Swiper only added a flaky cssMode snap grid that made swipe/arrows 454 // barely move. Pure overflow-x scroll needs no measurement and is robust. 455 // Desktop keeps Swiper below (hover lift relies on its overflow-visible layout). 456 if (useNativeScroll) { 457 if (this._nativePacks) return; 458 this._nativePacks = true; 459 460 const scroller = this; 461 scroller.style.overflow = ''; 462 scroller.style.overflowX = 'auto'; 463 scroller.style.overflowY = 'hidden'; 464 // Allow BOTH axes: horizontal pans the carousel, vertical falls through to 465 // the page (the row has overflow-y:hidden). Bare pan-x blocks page scroll; 466 // auto lets iOS's page scroll steal the horizontal gesture. 467 scroller.style.touchAction = 'pan-x pan-y'; 468 scroller.style.webkitOverflowScrolling = 'touch'; 469 scroller.style.scrollBehavior = 'smooth'; 470 471 const updatePacksBlur = function () { 472 scroller.classList.toggle('is-blur-left', scroller.scrollLeft > 2); 473 scroller.classList.toggle( 474 'is-blur-no-right', 475 scroller.scrollLeft + scroller.clientWidth >= scroller.scrollWidth - 2 476 ); 477 }; 478 scroller.addEventListener('scroll', updatePacksBlur, { passive: true }); 479 updatePacksBlur(); 480 481 const stepBy = function () { 482 const slide = scroller.querySelector('.swiper-slide'); 483 return slide ? slide.getBoundingClientRect().width + 12 : Math.round(scroller.clientWidth * 0.8); 484 }; 485 const bindArrow = function (btn, direction) { 486 if (!btn) return; 487 btn.addEventListener('click', function (e) { 488 e.preventDefault(); 489 scroller.scrollBy({ left: direction * stepBy(), behavior: 'smooth' }); 490 }); 491 }; 492 bindArrow($section.find('.swiper-button-next')[0], 1); 493 bindArrow($section.find('.swiper-button-prev')[0], -1); 494 return; 495 } 496 497 const wrapper = this.querySelector('.swiper-wrapper'); 498 // Clear the pre-render guard styles before Swiper takes 499 // over so its inline transform / sizing isn't fighting our 500 // fallback flex layout. 501 this.style.overflow = ''; 502 if (wrapper) { 503 wrapper.style.display = ''; 504 wrapper.style.flexWrap = ''; 505 wrapper.style.gap = ''; 506 } 507 // Edge blur follows scroll position: left fade off at the start, right off at the end. 508 const updatePacksBlur = function (swiper) { 509 const el = swiper.el; 510 el.classList.toggle("is-blur-left", !swiper.isBeginning); 511 el.classList.toggle("is-blur-no-right", swiper.isEnd); 512 }; 513 trackSwiper(new Swiper(this, { 514 // Native scroll on touch so the packs strip swipes back from the end on iOS 515 // mobile while still having a real start/end (see the game-row config above). 516 cssMode: useNativeScroll, 517 observer: true, 518 observeParents: true, 519 slidesPerView: "auto", 520 spaceBetween: 12, 521 navigation: { 522 nextEl: $section.find(".swiper-button-next")[0], 523 prevEl: $section.find(".swiper-button-prev")[0], 524 }, 525 wrapperClass: "swiper-wrapper", 526 slideClass: "swiper-slide", 527 on: { 528 init: updatePacksBlur, 529 setTranslate: updatePacksBlur, 530 resize: updatePacksBlur, 531 observerUpdate: updatePacksBlur, 532 }, 533 })); 534 }); 535 536 // Recent Wins â full Swiper with autoplay, nav arrows, and 537 // touch swipe so it scrolls like every other carousel on the 538 // page. Autoplay drives the marquee feel; pauseOnMouseEnter 539 // halts it while the cursor is over the strip and touch / nav 540 // taps take over on mobile. 541 const winnerCardsElement = document.querySelector(".winner__cards"); 542 if (winnerCardsElement && !winnerCardsElement.swiper) { 543 const wrapper = winnerCardsElement.querySelector('.swiper-wrapper'); 544 // Clear the pre-render guard styles before Swiper takes 545 // over so it can manage transforms / sizing cleanly. 546 winnerCardsElement.style.overflow = ''; 547 if (wrapper) { 548 wrapper.style.display = ''; 549 wrapper.style.flexWrap = ''; 550 wrapper.style.gap = ''; 551 } 552 const $section = $(winnerCardsElement).closest('.winner'); 553 const winnerSwiper = trackSwiper(new Swiper(winnerCardsElement, { 554 observer: true, 555 observeParents: true, 556 slidesPerView: 'auto', 557 spaceBetween: 12, 558 loop: true,
559 speed: 800, 560 allowTouchMove: true, 561 // Stepwise autoplay (delay 2.5s) so nav-arrow / touch 562 // clicks land in the idle gap between transitions â 563 // delay:0 + chained slideNext blocks user input. 564 autoplay: { 565 delay: 2500, 566 disableOnInteraction: false, 567 }, 568 navigation: { 569 nextEl: $section.find('.swiper-button-next')[0], 570 prevEl: $section.find('.swiper-button-prev')[0], 571 }, 572 wrapperClass: 'swiper-wrapper', 573 slideClass: 'swiper-slide', 574 })); 575 576 // Instant pause on hover â Swiper's built-in 577 // pauseOnMouseEnter lets the current transition finish 578 // before pausing, which feels sluggish. Cancelling the 579 // transition and snapping to the current rendered translate 580 // freezes the strip immediately. 581 let winnerPaused = false; 582 const winnerPause = () => { 583 if (winnerPaused) return; 584 winnerPaused = true; 585 const x = winnerSwiper.getTranslate(); 586 winnerSwiper.autoplay.stop(); 587 winnerSwiper.setTransition(0); 588 winnerSwiper.setTranslate(x); 589 }; 590 const winnerResume = () => { 591 if (!winnerPaused) return; 592 winnerPaused = false; 593 // Restore the standard transition and let autoplay 594 // schedule the next stepwise tick after its delay. 595 winnerSwiper.setTransition(winnerSwiper.params.speed); 596 winnerSwiper.autoplay.start(); 597 }; 598 winnerCardsElement.addEventListener('mouseenter', winnerPause); 599 winnerCardsElement.addEventListener('mouseleave', winnerResume); 600 } 601 602 // Check if Swiper is already initialized on providers swiper 603 const providersElement = document.querySelector('.providers-swiper'); 604 if (providersElement && providersElement.swiper) { 605 console.warn('Swiper already initialized on providers swiper, skipping') 606 } else if (providersElement) { 607 new Swiper('.providers-swiper', { 608 slidesPerView: '3', 609 spaceBetween: 10, 610 loop: true, 611 autoplay: { 612 delay: 0 613 }, 614 freeMode: true, 615 speed: 5000, 616 direction: 'horizontal', 617 grabCursor: true, 618 breakpoints: { 619 320: { 620 slidesPerView: 3.5, 621 }, 622 480: { 623 slidesPerView: 3.5, 624 }, 625 768: { 626 slidesPerView: 4.5, 627 }, 628 1024: { 629 slidesPerView: 5.5, 630 } 631 } 632 }); 633 } 634 } 635 636 function openMobileMenu() { 637 const navbar = $('.navbar')[0]; 638 if (navbar) { 639 // Reset transform and transition when opening 640 navbar.style.transform = ''; 641 navbar.style.transition = ''; 642 } 643 $('.navbar').addClass('active-mobile'); 644 $('#mobile-menu-link').addClass('active'); 645 $('body').addClass('overflow-hidden'); 646 } 647 648 function setupMobileMenuDrag() { 649 const menu = document.getElementById('side-navigation-menu'); 650 const puller = document.getElementById('navigation-menu-puller'); 651 setupDragHandler(menu, puller, () => closeMenu('mobile')); 652 } 653 654 function initializeMenus() { 655 $(document).on('click', function(e) { 656 if (!$(e.target).closest('.header__notification, #notifications-list').length) { 657 $("#notifications-list").removeClass("active"); 658 } 659 }); 660 661 // Navbar head toggle 662 $(".navbar__head").off('click').on("click", function () { 663 $(this).toggleClass("active"); 664 $(this).next().slideToggle(300); 665 }); 666 667 // Burger menu toggle 668 $(".burger").off('click').on("click", function () { 669 $(this).children().toggleClass("active"); 670 $(".navbar").toggleClass("active"); 671 $("body").toggleClass("fixed"); 672 return false;
673 }); 674 675 $(document).off('click.menuClose').on("click.menuClose", function (e) { 676 if (!$(e.target).closest(".navbar").length && !$('.navbar').hasClass('active-mobile')) { 677 $(".burger").children().removeClass("active"); 678 $(".navbar").removeClass("active"); 679 $("body").removeClass("fixed"); 680 } 681 }); 682 683 // Mobile menu link 684 $('#mobile-menu-link').off('click').on('click', function () { 685 // Close profile menu if it's open 686 $('.profile-menu').removeClass('active'); 687 688 // Close social menu if it's open with animation 689 const socialMenu = document.querySelector('.social-menu-navbar'); 690 if (socialMenu && socialMenu.classList.contains('active')) { 691 closeMenu('social'); 692 } 693 694 if ($('.navbar').hasClass('active-mobile')) { 695 closeMenu('mobile'); 696 } else { 697 openMobileMenu(); 698 } 699 return false; 700 }); 701 702 // Social menu modal - using Stimulus controller now 703 $('#social-menu-modal-link').off('click').on('click', function() { 704 // Close profile menu if it's open 705 $('.profile-menu').removeClass('active'); 706 707 // This will be handled by the Stimulus controller 708 return false; 709 }); 710 711 // Setup mobile menu drag functionality 712 setupMobileMenuDrag(); 713 714 715 } 716 717 function initializeBonusCarousel() { 718 const el = document.getElementById('bonus-carousel'); 719 if (!el || el.dataset.init === 'true') return; 720 if (el.swiper) { 721 console.warn('Bonus carousel Swiper already initialized, skipping'); 722 return; 723 } 724 725 // Configurable settings with UX-optimized defaults 726 const AUTOPLAY_DELAY = parseInt(el.dataset.delay, 10) || 5000; // 5 seconds for short-text banners 727 const RESUME_AFTER = parseInt(el.dataset.resumeAfter, 10) || 2000; // 2 seconds after interaction 728 const TRANSITION_SPEED = parseInt(el.dataset.speed, 10) || 600; // Smooth transitions 729 730 let resumeTimeout = null; 731 let isHovering = false; 732 let isTouching = false; 733 let isInteracting = false; 734 735 const swiper = new Swiper(el, { 736 wrapperClass: 'swiper-wrapper', 737 slideClass: 'swiper-slide', 738 loop: true, 739 grabCursor: true, 740 slidesPerView: 1, 741 spaceBetween: 0, 742 speed: TRANSITION_SPEED, 743 // Enhanced autoplay configuration for better UX 744 autoplay: { 745 delay: AUTOPLAY_DELAY, 746 disableOnInteraction: false, // Keep autoplay running after manual interaction 747 pauseOnMouseEnter: false, // We handle hover manually for better control 748 stopOnLastSlide: false, // Continue looping 749 reverseDirection: false 750 }, 751 pagination: { 752 el: el.querySelector('.swiper-pagination'), 753 clickable: true 754 }, 755 navigation: { 756 nextEl: el.querySelector('.swiper-button-next'), 757 prevEl: el.querySelector('.swiper-button-prev') 758 }, 759 // Enhanced touch and keyboard support 760 touchRatio: 1, 761 touchAngle: 45, 762 simulateTouch: true, 763 allowTouchMove: true, 764 keyboard: { 765 enabled: true, 766 onlyInViewport: true 767 }, 768 }); 769 770 function clearResumeTimer() { 771 if (resumeTimeout) { 772 clearTimeout(resumeTimeout); 773 resumeTimeout = null; 774 } 775 } 776 777 function pauseImmediate() { 778 clearResumeTimer(); 779 isInteracting = true; 780 if (swiper.autoplay.running) { 781 swiper.autoplay.stop(); 782 } 783 } 784 785 function scheduleResume() { 786 clearResumeTimer(); 787 resumeTimeout = setTimeout(() => { 788 isInteracting = false; 789 // Only resume if not hovering and not touching 790 if (!isHovering && !isTouching && !isInteracting) { 791 if (!swiper.autoplay.running) { 792 swiper.autoplay.start(); 793 } 794 } 795 }, RESUME_AFTER); 796 } 797 798 // Enhanced hover behavior 799 el.addEventListener('mouseenter', () => { 800 isHovering = true; 801 if (swiper.autoplay.running) { 802 swiper.autoplay.stop(); 803 } 804 scheduleResume(); 805 }); 806 807 el.addEventListener('mouseleave', () => { 808 isHovering = false;
809 if (!isTouching && !isInteracting) { 810 scheduleResume(); 811 } 812 }); 813 814 // Enhanced touch behavior 815 swiper.on('touchStart', () => { 816 isTouching = true; 817 pauseImmediate(); 818 }); 819 820 swiper.on('touchEnd', () => { 821 isTouching = false; 822 if (!isHovering && !isInteracting) { 823 scheduleResume(); 824 } 825 }); 826 827 // Enhanced interaction handling for navigation and pagination 828 const navigationButtons = [el.querySelector('.swiper-button-next'), el.querySelector('.swiper-button-prev')]; 829 navigationButtons.forEach(btn => { 830 if (btn) { 831 btn.addEventListener('click', () => { 832 pauseImmediate(); 833 scheduleResume(); 834 }); 835 } 836 }); 837 838 const pagination = el.querySelector('.swiper-pagination'); 839 if (pagination) { 840 pagination.addEventListener('click', (ev) => { 841 if (ev.target.closest('.swiper-pagination-bullet')) { 842 pauseImmediate(); 843 scheduleResume(); 844 } 845 }); 846 } 847 848 // Handle keyboard navigation 849 el.addEventListener('keydown', (ev) => { 850 if (ev.key === 'ArrowLeft' || ev.key === 'ArrowRight') { 851 pauseImmediate(); 852 scheduleResume(); 853 } 854 }); 855 856 // Add autoplay progress indicator 857 swiper.on('autoplayStart', () => { 858 el.classList.add('swiper-autoplay'); 859 }); 860 861 swiper.on('autoplayStop', () => { 862 el.classList.remove('swiper-autoplay'); 863 }); 864 865 // Prevent CLS 866 function lockHeightToTallestSlide() { 867 const slides = Array.from(el.querySelectorAll('.swiper-slide')); 868 let maxH = 0; 869 slides.forEach(s => { 870 const h = s.getBoundingClientRect().height; 871 if (h > maxH) maxH = h; 872 }); 873 if (maxH > 0) el.style.minHeight = `${Math.ceil(maxH)}px`; 874 } 875 const imgs = Array.from(el.querySelectorAll('img')); 876 if (imgs.length === 0) lockHeightToTallestSlide(); 877 else { 878 let remaining = imgs.length; 879 imgs.forEach(img => { 880 if (img.complete) remaining--; 881 else { 882 img.addEventListener('load', () => { remaining--; if (remaining <= 0) lockHeightToTallestSlide(); }); 883 img.addEventListener('error', () => { remaining--; if (remaining <= 0) lockHeightToTallestSlide(); }); 884 } 885 }); 886 if (remaining <= 0) lockHeightToTallestSlide(); 887 } 888 window.addEventListener('resize', () => { setTimeout(lockHeightToTallestSlide, 150); }); 889 890 el.dataset.init = 'true'; 891 el.swiper = swiper; 892 } 893 894 895 896 function initializeSelects() { 897 // Initialize all select dropdowns 898 document.querySelectorAll('.select').forEach(select => { 899 const main = select.querySelector('.select_main'); 900 const variants = select.querySelectorAll('.select_variant'); 901 902 // Main click handler - toggle this select only 903 main.addEventListener('click', function(e) { 904 e.stopPropagation(); 905 const wasActive = select.classList.contains('active'); 906 907 // Close all other selects first 908 document.querySelectorAll('.select').forEach(s => { 909 if (s !== select) { 910 s.classList.remove('active'); 911 } 912 }); 913 914 // Toggle this select 915 select.classList.toggle('active', !wasActive); 916 }); 917 918 // Variant selection handlers
919 variants.forEach(variant => { 920 variant.addEventListener('click', function(e) { 921 e.stopPropagation(); 922 923 const icon = variant.querySelector('.select_variant_value_icon img'); 924 const text = variant.querySelector('.select_variant_value_text'); 925 const mainIcon = main.querySelector('.select_main_value_icon img'); 926 const mainText = main.querySelector('.select_main_value_text'); 927 928 if (icon && mainIcon) { 929 mainIcon.src = icon.src; 930 } 931 932 if (text && mainText) { 933 mainText.textContent = text.textContent; 934 } 935 936 select.querySelectorAll('.select_variant').forEach(v => { 937 v.classList.remove('active'); 938 }); 939 variant.classList.add('active'); 940 select.classList.remove('active'); 941 }); 942 }); 943 }); 944 } 945 946 function openModalByHash(hash) { 947 const modalId = MODAL_HASH_MAP[hash]; 948 if (!modalId) return; 949 950 const isLoggedIn = document.querySelector('meta[name="player-id"]'); 951 952 if (!isLoggedIn) { 953 // Set session variable for auto-open after login 954 fetch('/set_auto_open_modal', { 955 method: 'POST', 956 headers: { 957 'Content-Type': 'application/json', 958 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content 959 }, 960 body: JSON.stringify({ modal_type: hash }) 961 }).catch(err => { 962 console.error('Failed to set auto-open modal:', err); 963 }); 964 965 setTimeout(() => { 966 if (typeof $ !== 'undefined' && $.fancybox) { 967 $.fancybox.open({ 968 src: '#modalsign', 969 type: 'inline', 970 }); 971 } 972 }, 500); 973 } else { 974 let triggerButton = null; 975 976 if (modalId === 'dailyChallengeModal') { 977 triggerButton = document.querySelector('[data-controller="daily-challenges"][data-action*="openModal"]'); 978 } else if (modalId === 'bonusCodeModal') { 979 // Bonus code modal uses Stimulus controller 980 // First, try to find the button with Stimulus controller 981 triggerButton = document.querySelector('[data-controller="bonus-code"][data-action*="open"]'); 982 983 if (triggerButton) { 984 // Button found, click it to trigger Stimulus controller 985 setTimeout(() => { 986 triggerButton.click(); 987 }, 500); 988 return; 989 } 990 991 // If no button found, try to open modal directly using Bootstrap API 992 // This handles the case when modal exists but button is not in DOM 993 setTimeout(() => { 994 const modalElement = document.getElementById('bonusCodeModal'); 995 if (modalElement && typeof bootstrap !== 'undefined') { 996 const modal = bootstrap.Modal.getOrCreateInstance(modalElement); 997 modal.show(); 998 } 999 }, 500); 1000 return; 1001 } else { 1002 triggerButton = document.querySelector(`[data-bs-target="#${modalId}"]`); 1003 } 1004 1005 if (triggerButton) { 1006 setTimeout(() => { 1007 triggerButton.click(); 1008 }, 500); 1009 } 1010 } 1011 } 1012 1013 function initializeListeners(){ 1014 if (window.listenersInitialized) return; 1015 1016 window.listenersInitialized = true 1017 1018 window.addEventListener('hashchange', () => { 1019 const newHash = window.location.hash.substring(1); 1020 if (newHash && MODAL_HASH_MAP[newHash]) { 1021 openModalByHash(newHash); 1022 } else if (!newHash) { 1023 const modals = ['dailyChallengeModal', 'leaderboardModal', 'referralStatsModal', 'rakebackModal'];
1024 modals.forEach(modalId => { 1025 const modalElement = document.getElementById(modalId); 1026 if (modalElement) { 1027 const modal = bootstrap.Modal.getInstance(modalElement); 1028 if (modal) { 1029 modal.hide(); 1030 } 1031 } 1032 }); 1033 } 1034 }); 1035 1036 // Override fullscreen API on mobile devices to prevent true fullscreen for games 1037 const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; 1038 const isAndroid = /Android/.test(navigator.userAgent); 1039 1040 if (isIOS || isAndroid) { 1041 const originalRequestFullscreen = HTMLElement.prototype.requestFullscreen; 1042 const originalMozRequestFullScreen = HTMLElement.prototype.mozRequestFullScreen; 1043 const originalWebkitRequestFullscreen = HTMLElement.prototype.webkitRequestFullscreen; 1044 const originalMsRequestFullscreen = HTMLElement.prototype.msRequestFullscreen; 1045 1046 function interceptFullscreen() { 1047 // Check if this is being called on a game iframe or its contents 1048 const gameSection = this.closest('.game'); 1049 const isInGameIframe = this.tagName === 'IFRAME' && this.hasAttribute('data-fullscreen-target'); 1050 1051 if ((gameSection || isInGameIframe) && window.wildFullscreenController) { 1052 // Use our mobile fullscreen instead 1053 window.wildFullscreenController.toggleMobileFullscreen(); 1054 return Promise.resolve(); 1055 } 1056 1057 // For non-game elements, use original fullscreen 1058 return originalRequestFullscreen.call(this); 1059 } 1060 1061 // Override all fullscreen methods 1062 HTMLElement.prototype.requestFullscreen = interceptFullscreen; 1063 if (originalMozRequestFullScreen) { 1064 HTMLElement.prototype.mozRequestFullScreen = interceptFullscreen; 1065 } 1066 if (originalWebkitRequestFullscreen) { 1067 HTMLElement.prototype.webkitRequestFullscreen = interceptFullscreen; 1068 } 1069 if (originalMsRequestFullscreen) { 1070 HTMLElement.prototype.msRequestFullscreen = interceptFullscreen; 1071 } 1072 } 1073 1074 window.addEventListener('message', (e) => { 1075 console.log(e.data); 1076 1077 if (e.data && typeof e.data['verification_status'] === 'string' && trustedKycMessageOrigin(e.origin)) { 1078 const status = e.data['verification_status']; 1079 1080 // verification.received (pending review) must not navigate â the entry point has to survive until the verdict. 1081 if ((status === 'verification.accepted' || status === 'verification.declined') && firstKycStatusHandling(status)) { 1082 const redeemEntry = sessionStorage.getItem('kycEntryPoint') === 'redeem'; 1083 sessionStorage.removeItem('kycEntryPoint'); 1084 1085 if (status === 'verification.accepted' && redeemEntry) { 1086 sessionStorage.setItem('openRedeemPending', '1'); 1087 Turbo.visit('/me?tab=player&subtab=balance'); 1088 } else { 1089 Turbo.visit('/me?tab=player&subtab=proof_of_identification'); 1090 } 1091 } 1092 } 1093 1094 // Handle fullscreen requests from game iframes (mobile only for Coinz) 1095 if (e.data && (e.data.type === 'fullscreen' || e.data.action === 'fullscreen' || e.data === 'fullscreen')) { 1096 const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; 1097 const isAndroid = /Android/.test(navigator.userAgent); 1098 1099 // Only handle on mobile devices to prevent true fullscreen 1100 if ((isIOS || isAndroid) && window.wildFullscreenController) { 1101 e.preventDefault(); 1102 e.stopPropagation(); 1103 1104 // Use the mobile fullscreen instead of true fullscreen 1105 window.wildFullscreenController.toggleMobileFullscreen(); 1106 } 1107 } 1108 }); 1109 1110 // Close all selects when clicking outside 1111 document.addEventListener('click', function(e) { 1112 const select = e.target.closest('.select'); 1113 1114 console.log('clicked', e.target) 1115 1116 // Close mobile menu if clicked outside 1117 if ($('.navbar').hasClass('active-mobile')) { 1118 // Don't close if clicking on modals or their backdrops 1119 const isModalClick = e.target.closest('.fancybox-container') || 1120 e.target.closest('.modal') || 1121 e.target.classList.contains('fancybox-container') || 1122 e.target.classList.contains('modal-backdrop'); 1123 1124 if (!e.target.closest('.navbar') && 1125 !e.target.closest('#mobile-menu-link') && 1126 !isModalClick) { 1127 closeMenu('mobile'); 1128 } 1129 } 1130 1131 // Close social menu if clicked outside 1132 const socialMenu = document.querySelector('.social-menu-navbar'); 1133 if (socialMenu && socialMenu.classList.contains('active')) { 1134 // Don't close if clicking on modals or their backdrops 1135 const isModalClick = e.target.closest('.fancybox-container') || 1136 e.target.closest('.modal') || 1137 e.target.classList.contains('fancybox-container') || 1138 e.target.classList.contains('modal-backdrop'); 1139 1140 if (!e.target.closest('.social-menu-navbar') && 1141 !e.target.closest('#social-menu-modal-link') && 1142 !isModalClick) { 1143 closeMenu('social'); 1144 } 1145 } 1146 1147 // Handle profile menu toggle and close 1148 if (e.target.closest('.header__profile')) { 1149 // Toggle profile menu on profile button click 1150 $(".profile-menu").toggleClass("active"); 1151 } else if (e.target.closest('.switch') || e.target.closest('#checkbox2')) { 1152 // Don't close profile menu when clicking on theme switch
1153 // Do nothing, keep menu open 1154 } else if ($('.profile-menu').hasClass('active') && !e.target.closest('.profile-menu')) { 1155 // Close profile menu if clicked outside 1156 $('.profile-menu').removeClass('active'); 1157 } 1158 1159 // If clicked outside any select, close all 1160 if (!select) { 1161 document.querySelectorAll('.select').forEach(s => { 1162 s.classList.remove('active'); 1163 }); 1164 } 1165 // If clicked on a select_main, let its handler manage the toggle 1166 else if (e.target.closest('.select_main')) { 1167 return; // Let the select_main click handler deal with it 1168 } 1169 // If clicked elsewhere in an open select (but not on select_main) 1170 else { 1171 document.querySelectorAll('.select').forEach(s => { 1172 if (s !== select) { 1173 s.classList.remove('active'); 1174 } 1175 }); 1176 } 1177 }); 1178 1179 // Listen for successful deposit events from PaymentIQ cashier 1180 document.addEventListener('deposit', function(e) { 1181 handleDepositSuccess(e.detail); 1182 }); 1183 1184 document.addEventListener('first_time_deposit', function(e) { 1185 handleDepositSuccess(e.detail); 1186 }); 1187 1188 function handleDepositSuccess(detail) { 1189 // Close the modal 1190 if (typeof $.fancybox !== 'undefined') { 1191 $.fancybox.close(); 1192 } 1193 1194 // Handle package_shop context redirect 1195 if (window.purchaseContext === 'package_shop') { 1196 setTimeout(() => { 1197 window.location.href = '/en'; 1198 }, 1500); 1199 return; 1200 } 1201 1202 // Only refresh if path doesn't contain "game" keyword 1203 const currentPath = window.location.pathname.toLowerCase(); 1204 if (!currentPath.includes('game')) { 1205 setTimeout(() => { 1206 if (typeof Turbo !== 'undefined') { 1207 Turbo.visit(window.location.href, { action: 'replace' }); 1208 } else { 1209 window.location.reload(); 1210 } 1211 }, 1500); 1212 } 1213 } 1214 1215 document.addEventListener('click', function(e) { 1216 // Handle .open-deposit-modal clicks 1217 const depositModalBtn = e.target.closest('.open-deposit-modal'); 1218 if (depositModalBtn) { 1219 e.preventDefault(); 1220 e.stopPropagation(); 1221 1222 const packageId = depositModalBtn.dataset.packageId; 1223 const bonusId = depositModalBtn.dataset.bonusId; 1224 const pair = depositModalBtn.dataset.pair; 1225 1226 window.purchaseContext = window.location.pathname.includes('/purchases') ? 'package_shop' : 'in_game'; 1227 1228 // Check if Coinflow cashier is enabled 1229 if (document.querySelector('[data-controller="coinflow-cashier"]')) { 1230 window.dispatchEvent(new CustomEvent('open-coinflow-cashier', { detail: { packageId: packageId, bonusId: bonusId, pair: pair } })); 1231 return; 1232 } 1233 1234 // Check if Fluid cashier is enabled 1235 if (document.querySelector('[data-controller="fluid-widget"]')) { 1236 window.dispatchEvent(new CustomEvent('open-fluid-cashier', { detail: { packageId: packageId, pair: pair } })); 1237 return; 1238 } 1239 1240 const url = `/en/deposits/new_fiat?coins_package_id=${packageId}&bonus_id=${bonusId}&render_layout=false`; 1241 1242 $.fancybox.open({ 1243 src: url, 1244 type: 'ajax', 1245 opts: { 1246 afterShow: function(instance, current) { 1247 console.log('Modal loaded successfully!'); 1248 }, 1249 afterClose: function() { 1250 if (typeof xtremepush === 'function') { 1251 xtremepush('event', 'purchase_popup_closed'); 1252 } 1253 } 1254 } 1255 }); 1256 return; 1257 } 1258 1259 const referralButton = e.target.closest('#copyReferralButton'); 1260 if (referralButton) { 1261 const copyText = document.getElementById("referralLink"); 1262 1263 navigator.clipboard.writeText(copyText.value).then(() => { 1264 const button = e.target; 1265 const originalText = button.textContent; 1266 button.textContent = 'Copied!'; 1267 1268 setTimeout(() => { 1269 button.textContent = originalText; 1270 }, 2000); 1271 }).catch(err => { 1272 console.error('Failed to copy text: ', err); 1273 }); 1274 } 1275 1276 // Handle #buy-button clicks 1277 const buyButton = e.target.closest('#buy-button'); 1278 if (buyButton) { 1279 e.preventDefault(); 1280 e.stopPropagation(); 1281 1282 const amount = document.getElementById('amount-input')?.value; 1283 if (!amount) return; 1284 1285 const url = `/en/deposits/new_fiat?amount=${amount}&render_layout=false&build_package=true`; 1286 1287 $.fancybox.open({ 1288 src: url, 1289 type: 'ajax', 1290 opts: { 1291 afterShow: function(instance, current) { 1292 console.log('Modal loaded successfully!'); 1293 } 1294 } 1295 }); 1296 return; 1297 } 1298 1299 // Handle [data-fancybox] clicks 1300 const fancyboxTrigger = e.target.closest('[data-fancybox]'); 1301 if (fancyboxTrigger) { 1302 console.log('fancybox111') 1303 e.preventDefault(); 1304 e.stopPropagation(); 1305 1306 const targetModal = fancyboxTrigger.getAttribute('href'); 1307 const fancyboxOptions = { 1308 src: targetModal, 1309 type: "inline" 1310 }; 1311 1312 if (targetModal === '#modalreg') { 1313 fancyboxOptions.afterClose = function() { 1314 window.dispatchEvent(new CustomEvent('registrationModalClosed')); 1315 }; 1316 } 1317 1318 if ($.fancybox.getInstance()) { 1319 $.fancybox.close(); 1320 1321 setTimeout(() => { 1322 $.fancybox.open(fancyboxOptions); 1323 }, 300); 1324 } else { 1325 $.fancybox.open(fancyboxOptions); 1326 } 1327 return; 1328 } 1329 }); 1330 } 1331 1332 initializeListeners(); 1333 1334 Object.entries(MODAL_HASH_MAP).forEach(([hashKey, modalId]) => { 1335 const modalElement = document.getElementById(modalId); 1336 if (modalElement && !modalElement.dataset.hashListenerAttached) { 1337 modalElement.dataset.hashListenerAttached = 'true'; 1338 1339 modalElement.addEventListener('shown.bs.modal', function() { 1340 console.log(`Modal shown: ${modalId}, adding hash: #${hashKey}`); 1341 if (window.location.hash !== `#${hashKey}`) { 1342 history.pushState(null, null, `#${hashKey}`); 1343 } 1344 }); 1345 1346 modalElement.addEventListener('hidden.bs.modal', function() { 1347 console.log(`Modal hidden: ${modalId}, removing hash`); 1348 if (window.location.hash === `#${hashKey}`) { 1349 history.pushState(null, null, window.location.pathname + window.location.search); 1350 } 1351 }); 1352 } 1353 }); 1354 1355 const hash = window.location.hash.substring(1); 1356 if (hash && MODAL_HASH_MAP[hash]) { 1357 openModalByHash(hash); 1358 } 1359 1360 // Check for auto-open modal flags from session 1361 function checkAutoOpenModals() { 1362 const autoOpenDailyquest = document.querySelector('meta[name="auto-open-dailyquest-modal"]')?.content === 'true'; 1363 const autoOpenRaces = document.querySelector('meta[name="auto-open-races-modal"]')?.content === 'true'; 1364 const autoOpenRefer = document.querySelector('meta[name="auto-open-refer-modal"]')?.content === 'true'; 1365 const autoOpenBonusCode = document.querySelector('meta[name="auto-open-bonus-code
1365-modal"]')?.content === 'true'; 1366 const autoOpenCoinzback = document.querySelector('meta[name="auto-open-coinzback-modal"]')?.content === 'true'; 1367 1368 if (autoOpenDailyquest) { 1369 setTimeout(() => { 1370 openModalByHash('dailyquest'); 1371 }, 500); 1372 } else if (autoOpenRaces) { 1373 setTimeout(() => { 1374 openModalByHash('races'); 1375 }, 500); 1376 } else if (autoOpenRefer) { 1377 setTimeout(() => { 1378 openModalByHash('refer'); 1379 }, 500); 1380 } else if (autoOpenBonusCode) { 1381 setTimeout(() => { 1382 openModalByHash('bonus_code'); 1383 }, 500); 1384 } else if (autoOpenCoinzback) { 1385 setTimeout(() => { 1386 openModalByHash(RAKEBACK_HASH); 1387 }, 500); 1388 } 1389 } 1390 1391 // Check auto-open modals after page load 1392 checkAutoOpenModals(); 1393 1394 // The verified Redeem trigger renders only after the Shufti webhook lands â poll quietly, reload once it has. 1395 if (sessionStorage.getItem('openRedeemPending') === '1') { 1396 const redeemTrigger = document.querySelector('[data-redeem-trigger]'); 1397 1398 if (redeemTrigger) { 1399 sessionStorage.removeItem('openRedeemPending'); 1400 setTimeout(() => { 1401 redeemTrigger.click(); 1402 }, 500); 1403 } else { 1404 const pollUrl = window.location.href; 1405 let attempts = 0; 1406 1407 const pollForRedeemTrigger = () => { 1408 if (window.location.href !== pollUrl) return; 1409 1410 fetch(pollUrl) 1411 .then((response) => response.text()) 1412 .then((html) => { 1413 if (html.includes('data-redeem-trigger')) { 1414 Turbo.visit(pollUrl); 1415 } else if (++attempts < 5) { 1416 setTimeout(pollForRedeemTrigger, 2000); 1417 } else { 1418 sessionStorage.removeItem('openRedeemPending'); 1419 } 1420 }) 1421 .catch(() => sessionStorage.removeItem('openRedeemPending')); 1422 }; 1423 1424 setTimeout(pollForRedeemTrigger, 2000); 1425 } 1426 } 1427 1428 // Handle ?modal= query parameter for registration and signin 1429 const urlParams = new URLSearchParams(window.location.search); 1430 const modalParam = urlParams.get('modal'); 1431 if (modalParam && (modalParam === 'registration' || modalParam === 'signin')) { 1432 const modalId = modalParam === 'registration' ? '#modalreg' : '#modalsign'; 1433 setTimeout(() => { 1434 if (typeof $ !== 'undefined' && $.fancybox) { 1435 if (modalId === '#modalreg') { 1436 $.fancybox.open({ 1437 src: modalId, 1438 type: 'inline', 1439 opts: { 1440 afterClose: function() { 1441 window.dispatchEvent(new CustomEvent('registrationModalClosed')); 1442 } 1443 } 1444 }); 1445 } else { 1446 $.fancybox.open({ 1447 src: modalId, 1448 type: 'inline' 1449 }); 1450 } 1451 } 1452 }, 500); 1453 1454 // Remove modal param from URL after opening 1455 urlParams.delete('modal'); 1456 const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : ''); 1457 window.history.replaceState({}, document.title, newUrl); 1458 } 1459 1460 initializeSelects(); 1461 1462 initializeSliders(); 1463 // Exposed so Turbo-Stream navigations (store_navigation_controller replaces #games without a 1464 // turbo:load) can re-init game-row swipers inserted into the new content. Idempotent. 1465 window.wildInitializeSliders = initializeSliders; 1466 initializeMenus(); 1467 initializeBonusCarousel(); 1468}) 1469 1470$(document).on("ready turbo:load turbo:frame-render", function() { 1471 const sections = document.querySelectorAll('.account__content--second > div'); 1472 1473 // Initialize edit buttons for inputs 1474 document.querySelectorAll('.account__input--edit').forEach(editButton => { 1475 const freshEditButton = editButton.parentNode.querySelector('.account__input--edit'); 1476 1477 freshEditButton.addEventListener('click', function() { 1478 const inputField = this.previousElementSibling; 1479 1480 if (inputField && inputField.matches('input, textarea')) { 1481 inputField.readOnly = false;
1482 inputField.classList.toggle('active'); 1483 this.style.display = 'none'; 1484 inputField.focus(); 1485 1486 // Optional: Handle when input loses focus 1487 const handleBlur = () => { 1488 inputField.readOnly = true; 1489 inputField.classList.remove('active'); 1490 freshEditButton.style.display = ''; 1491 inputField.removeEventListener('blur', handleBlur); 1492 }; 1493 1494 inputField.addEventListener('blur', handleBlur); 1495 } 1496 }); 1497 }); 1498}) 1499 1500// workaround for redemption form, for some reason events not fired on mobiles 1501window.addEventListener('touchend', function (e) { 1502 const btn = e.target.closest('#modalwithdrawal .withdrawal_button[type="submit"]'); 1503 console.log('tochend fired', btn) 1504 if (!btn) return; 1505 1506 e.preventDefault(); 1507 e.stopPropagation(); 1508 1509 const form = btn.form; 1510 if (form?.requestSubmit) form.requestSubmit(); 1511 else if (form) form.submit(); 1512}, { capture: true, passive: false });
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.