1jQuery(function ($) { 2 $(document).on('click', '.see-more-testimonials-button', function () { 3 4 var testimonialsContainer = $(this).closest('.testimonials'); 5 var child = $(this).closest('.testimonials-section'); // â plural & matches markup! 6 var gradient = testimonialsContainer.find('.testimonial-gradient'); 7 8 if (child.hasClass('max-h-[600px]')) { 9 gradient.hide(); 10 child.removeClass('max-h-[600px]'); 11 $(this).text('Hide Testimonials'); 12 } else { 13 gradient.show(); 14 child.addClass('max-h-[600px]'); 15 $(this).text('See More Testimonials'); 16 } 17 18 return false; 19 }); 20 });; 21const prevIcon = '<svg fill="none" height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg" q:key="1x_0"><path clip-rule="evenodd" d="M11.7623 6.22456C11.5182 5.98048 11.1224 5.98048 10.8784 6.22456L8.13429 8.96864C7.56478 9.53815 7.56478 10.4615 8.13429 11.031L10.8784 13.7751C11.1224 14.0192 11.5182 14.0192 11.7623 13.7751C12.0063 13.531 12.0063 13.1353 11.7623 12.8912L9.01818 10.1472C8.93682 10.0658 8.93682 9.93388 9.01818 9.85252L11.7623 7.10845C12.0063 6.86437 12.0063 6.46864 11.7623 6.22456Z" fill="#11244A" fill-rule="evenodd"></path></svg>'; 22const nextIcon = '<svg fill="none" height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg" q:key="UQ_0"><path clip-rule="evenodd" d="M7.89009 6.22456C8.13417 5.98048 8.5299 5.98048 8.77397 6.22456L11.5181 8.96864C12.0876 9.53815 12.0876 10.4615 11.5181 11.031L8.77397 13.7751C8.5299 14.0192 8.13417 14.0192 7.89009 13.7751C7.64601 13.531 7.64601 13.1353 7.89009 12.8912L10.6342 10.1472C10.7155 10.0658 10.7155 9.93388 10.6342 9.85252L7.89009 7.10845C7.64601 6.86437 7.64601 6.46864 7.89009 6.22456Z" fill="#11244A" fill-rule="evenodd"></path></svg>'; 23 24function featureSlider() { 25 return { 26 sliderInitialized: false, 27 sliderInView: false, 28 29 initSlider() { 30 const self = this; // Store reference for use in callbacks 31 32 // Use IntersectionObserver to detect when slider container is in view 33 const sliderContainer = document.querySelector('.feature-slider'); 34 if (!sliderContainer) return; 35 36 const sliderObserver = new IntersectionObserver((entries) => { 37 if (entries[0].isIntersecting && !self.sliderInView) { 38 self.sliderInView = true; 39 // If slider is already initialized, load images immediately 40 if (self.sliderInitialized) { 41 self.lazyLoadVisibleSlides(); 42 } 43 } 44 }, { 45 rootMargin: '300px', // Start loading well before slider is visible 46 threshold: 0 47 }); 48 49 sliderObserver.observe(sliderContainer); 50 51 Alpine.nextTick(() => { 52 const $slider = jQuery('.feature-slider'); 53 54 $slider.slick({ 55 arrows: true, 56 prevArrow: '<button class="slick-prev slick-arrow" aria-label="Previous" type="button">' + prevIcon + 'Previous</button>', 57 nextArrow: '<button class="slick-next slick-arrow" aria-label="Next" type="button">' + nextIcon + 'Next</button>', 58 autoplay: true, 59 autoplaySpeed: 6000, 60 infinite: false, 61 speed: 300, 62 slidesToShow: 2.5, 63 slidesToScroll: 1, 64 responsive: [ 65 { 66 breakpoint: 1024, 67 settings: { 68 slidesToShow: 2, 69 slidesToScroll: 1, 70 } 71 }, 72 { 73 breakpoint: 768, 74 settings: { 75 slidesToShow: 1, 76 slidesToScroll: 1, 77 } 78 } 79 ] 80 }); 81 82 self.sliderInitialized = true; 83 84 // Load images immediately if slider is already in view 85 if (self.sliderInView) { 86 self.lazyLoadVisibleSlides(); 87 } 88 89 // Load images when slider initializes 90 $slider.on('init', function () { 91 self.lazyLoadVisibleSlides(); 92 }); 93 94 // Preload images BEFORE slide change to ensure they're ready 95 $slider.on('beforeChange', function () { 96 self.lazyLoadVisibleSlides(); 97 }); 98 99 // Also load on afterChange as backup 100 $slider.on('afterChange', function () { 101 self.lazyLoadVisibleSlides(); 102 }); 103 }); 104 }, 105 106 lazyLoadVisibleSlides() { 107 const $slider = jQuery('.feature-slider'); 108 if (!$slider.length || !$slider.slick('getSlick')) return; 109 110 try { 111 const currentIndex = $slider.slick('slickCurrentSlide') || 0; 112 const slidesToShow = $slider.slick('getSlick').options.slidesToShow || 1; 113 const totalSlides = $slider.slick('getSlick').slideCount || 0; 114 115 // Load images for visible slides plus 3 ahead for aggressive preloading 116 const slidesToLoad = Math.ceil(slidesToShow) + 3; 117 118 for (let i = 0; i < slidesToLoad; i++) { 119 const slideIndex = currentIndex + i; 120 if (slideIndex >= totalSlides) break; 121 122 const slideElement = $slider.find('[data-sli
122de-index="' + slideIndex + '"]').first()[0]; 123 if (!slideElement) continue; 124 125 // Access Alpine component data to trigger lazy loading 126 if (slideElement._x_dataStack && slideElement._x_dataStack.length > 0) { 127 const alpineData = slideElement._x_dataStack[0]; 128 if (alpineData && typeof alpineData.loadBackground === 'function' && !alpineData.loaded) { 129 alpineData.loadBackground(); 130 } 131 } 132 } 133 } catch (e) { 134 console.warn('Error loading slider images:', e); 135 } 136 } 137 } 138}; 139// In alpineFunctions.js 140/** 141 * Alpine Functions 142 * This file contains the Alpine functions for the Prep Network Content theme 143 */ 144document.addEventListener('alpine:init', () => { 145 window.alpineFunctions = window.alpineFunctions || {}; 146 /** 147 * Background Image Component 148 * This component is used to load the background image 149 * It uses the srcset to load the image and the fallback to load the image if the srcset is not available 150 * It also uses the debouncedUpdate to update the image when the window is resized 151 */ 152 window.alpineFunctions.backgroundImageComponent = function (srcset = '', fallback = '') { 153 return { 154 srcset, 155 fallback, 156 currentSrc: fallback, 157 _debounceTimeout: null, 158 159 init() { 160 this.update(); 161 window.addEventListener('resize', this.debouncedUpdate.bind(this)); 162 }, 163 update() { 164 if (!this.srcset) { 165 return; 166 } 167 const sources = Array.from(this.srcset.matchAll(/(https?:\/\/.+?)(?=\s+\d+w)/g)).map((url, i, arr) => { 168 const widths = [...this.srcset.matchAll(/(\d+)w/g)]; 169 return { 170 url: url[1], 171 width: parseInt(widths[i][1]), 172 }; 173 }).filter(source => source.width !== source.height); 174 sources.sort((a, b) => a.width - b.width); 175 const viewport = window.innerWidth; 176 let selected = sources[sources.length - 1]?.url || this.fallback; 177 for (const source of sources) { 178 if (viewport <= source.width) { 179 selected = source.url; 180 break; 181 } 182 } 183 this.currentSrc = selected; 184 }, 185 debouncedUpdate() { 186 clearTimeout(this._debounceTimeout); 187 this._debounceTimeout = setTimeout(() => { 188 this.update(); 189 }, 250); 190 } 191 }; 192 }; 193 194 /** 195 * Lazy Background Component 196 * This component is used to lazy load the background image 197 * It uses the Intersection Observer API to load the image when it is in view 198 * It also uses the backgroundImageComponent to handle the image loading and resizing 199 */ 200 Alpine.data('lazyBackgroundComponent', (srcset, fallback) => ({ 201 observer: null, 202 loaded: false, 203 child: null, // this will hold the backgroundImageComponent instance 204 resizeHandler: null, // store handler reference for cleanup 205 syncTimeout: null, // store timeout reference for cleanup 206 isSliderSlide: false, // track if this is a slider slide 207 208 currentSrc: '', // exposed for Blade x-bind 209 210 init() { 211 // Check if element has data-slide-index attribute (slider slides) 212 this.isSliderSlide = this.$el.hasAttribute('data-slide-index'); 213 214 if (this.isSliderSlide) { 215 // For slider slides, set currentSrc to empty initially 216 // They will be loaded manually via Slick events 217 this.currentSrc = ''; 218 // Don't create IntersectionObserver - will be loaded manually 219 return; 220 } 221 222 // For non-slider elements, use fallback and IntersectionObserver 223 this.currentSrc = fallback; 224 225 this.observer = new IntersectionObserver((entries) => { 226 if (entries[0].isIntersecting) { 227 this.loadBackground(); 228 this.observer.disconnect(); 229 } 230 }, { 231 rootMargin: '200px', // preload slightly before visibility 232 }); 233 234 this.observer.observe(this.$el); 235 }, 236
237 loadBackground() { 238 if (this.loaded) return; 239 this.loaded = true; 240 241 // initialize the responsive component dynamically 242 this.child = window.alpineFunctions.backgroundImageComponent(srcset, fallback); 243 244 // Manually run its init + update routine 245 this.child.init(); 246 this.child.update(); 247 248 // Sync the background to the wrapper value 249 this.currentSrc = this.child.currentSrc; 250 251 // Preload the image to improve perceived load time 252 if (this.currentSrc) { 253 const img = new Image(); 254 img.src = this.currentSrc; 255 } 256 257 // Keep them in sync on resize 258 // Store handler reference for cleanup 259 this.resizeHandler = () => { 260 if (!this.child) return; // safety check 261 this.child.debouncedUpdate(); 262 // Clear any pending sync timeout 263 if (this.syncTimeout) { 264 clearTimeout(this.syncTimeout); 265 } 266 // Sync after debounce completes (250ms delay) 267 this.syncTimeout = setTimeout(() => { 268 if (this.child) { 269 this.currentSrc = this.child.currentSrc; 270 } 271 this.syncTimeout = null; 272 }, 250); 273 }; 274 window.addEventListener('resize', this.resizeHandler); 275 }, 276 277 destroy() { 278 // Cleanup: remove resize listener, clear timeouts, and disconnect observer 279 if (this.resizeHandler) { 280 window.removeEventListener('resize', this.resizeHandler); 281 this.resizeHandler = null; 282 } 283 if (this.syncTimeout) { 284 clearTimeout(this.syncTimeout); 285 this.syncTimeout = null; 286 } 287 if (this.observer) { 288 this.observer.disconnect(); 289 this.observer = null; 290 } 291 } 292 })); 293 294 /** 295 * Color Utils 296 * Global helper functions for working with colors 297 * - createGradientClass: Creates a gradient class for an element 298 * - clearGradientClass: Removes a dynamic gradient from an element 299 * - rotateHueAndLighten: Rotates a hue and lightens a color 300 * - darken: Darkens a color 301 * 302 * Gradient usage: 303 * <div x-data x-init=" 304 * $el.style.setProperty('--primary', $colorUtils.getVar('--site-primary')); 305 * $colorUtils.createGradientClass($el, 350, 44); 306 * "> 307 * $el is the element that the gradient class is being applied to 308 * --primary is the variable that is being set to the site primary color 309 * 350 is the hue offset 310 * 44 is the lighten amount 311 * 312 */ 313 Alpine.magic('colorUtils', () => ({ 314 getVar(name) { 315 return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); 316 }, 317 /** 318 * hexToHsl: Converts a hex color to an hsl color 319 */ 320 hexToHsl(hex) { 321 hex = hex.replace('#', ''); 322 let r = parseInt(hex.slice(0, 2), 16) / 255; 323 let g = parseInt(hex.slice(2, 4), 16) / 255;
324 let b = parseInt(hex.slice(4, 6), 16) / 255; 325 326 let max = Math.max(r, g, b), min = Math.min(r, g, b); 327 let h, s, l = (max + min) / 2; 328 329 if (max === min) { 330 h = s = 0; 331 } else { 332 let d = max - min; 333 s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 334 switch (max) { 335 case r: h = (g - b) / d + (g < b ? 6 : 0); break; 336 case g: h = (b - r) / d + 2; break; 337 case b: h = (r - g) / d + 4; break; 338 } 339 h /= 6; 340 } 341 342 return { 343 h: Math.round(h * 360), 344 s: Math.round(s * 100), 345 l: Math.round(l * 100), 346 }; 347 }, 348 349 /** 350 * hslToHex: Converts an hsl color to a hex color 351 */ 352 hslToHex(h, s, l) { 353 s /= 100; 354 l /= 100; 355 356 const c = (1 - Math.abs(2 * l - 1)) * s; 357 const x = c * (1 - Math.abs((h / 60) % 2 - 1)); 358 const m = l - c / 2; 359 360 let r, g, b; 361 if (h < 60) [r, g, b] = [c, x, 0]; 362 else if (h < 120) [r, g, b] = [x, c, 0]; 363 else if (h < 180) [r, g, b] = [0, c, x]; 364 else if (h < 240) [r, g, b] = [0, x, c]; 365 else if (h < 300) [r, g, b] = [x, 0, c]; 366 else [r, g, b] = [c, 0, x]; 367 368 r = Math.round((r + m) * 255); 369 g = Math.round((g + m) * 255); 370 b = Math.round((b + m) * 255); 371 372 return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`; 373 }, 374 /** 375 * rotateHueAndLighten: Rotates a hue and lightens a color 376 * this is based on wheel of colors so we match the right contrast for the color 377 * https://www.rapidtables.com/web/color/RGB_Color.html 378 * 0 is the starting point of the wheel of colors based on the color passed in 379 * 360 is the ending point of the wheel of colors 380 */ 381 rotateHueAndLighten(hex, hueOffset = 30, lightenAmount = 30) { 382 const hsl = this.hexToHsl(hex); 383 const newHue = (hsl.h + hueOffset) % 360; 384 /**Exeptions prep dig / redzone **/ 385 /** those colors were too dark so we lighten them up a bit manually */ 386 const exceptions = {213: 57, 357: 54}; 387 if(Object.keys(exceptions).includes(hsl.h.toString())) { 388 lightenAmount = exceptions[hsl.h.toString()]; 389 } 390 const newLightness = Math.min(100, hsl.l + lightenAmount); 391 return this.hslToHex(newHue, hsl.s, newLightness); 392 }, 393 /**
394 * darken: Darkens a color 395 */ 396 darken(hex, percent) { 397 hex = hex.replace('#', ''); 398 let r = parseInt(hex.substring(0, 2), 16); 399 let g = parseInt(hex.substring(2, 4), 16); 400 let b = parseInt(hex.substring(4, 6), 16); 401 402 r = Math.max(0, Math.floor(r * (1 - percent / 100))); 403 g = Math.max(0, Math.floor(g * (1 - percent / 100))); 404 b = Math.max(0, Math.floor(b * (1 - percent / 100))); 405 406 return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`; 407 }, 408 /** 409 * hexToRgba: Converts a hex color to rgba 410 */ 411 hexToRgba(hex, alpha) { 412 hex = hex.replace('#', ''); 413 if (hex.length === 3) { 414 hex = hex.split('').map((char) => char + char).join(''); 415 } 416 417 const r = parseInt(hex.slice(0, 2), 16); 418 const g = parseInt(hex.slice(2, 4), 16); 419 const b = parseInt(hex.slice(4, 6), 16); 420 421 return `rgba(${r}, ${g}, ${b}, ${alpha})`; 422 }, 423 /** 424 * createGradientClass: Creates a gradient class for an element 425 * 426 * @param {HTMLElement} el 427 * @param {number} hueOffset 428 * @param {number} lightenAmount 429 * @param {boolean} border 430 * @param {string} direction CSS linear-gradient direction (e.g. 'to right', 'to top') 431 * @param {number|null} opacity Optional alpha for a lighter transparent wash (0â1) 432 */ 433 createGradientClass(el, hueOffset = 30, lightenAmount = 30, border = true, direction = 'to right', opacity = null) { 434 const primary = this.getVar('--site-primary'); 435 const gradientDark = this.rotateHueAndLighten(primary, hueOffset+30, lightenAmount); 436 const gradientLight = this.rotateHueAndLighten(primary, hueOffset, lightenAmount-5); 437 const borderColor = this.rotateHueAndLighten(primary, 0, lightenAmount-3); 438 439 el.classList.add('dynamic-gradient'); 440 441 if (opacity !== null) { 442 el.style.backgroundImage = `linear-gradient(${direction}, ${this.hexToRgba(gradientDark, opacity)}, ${this.hexToRgba(gradientLight, opacity * 0.25)})`; 443 if (border) { 444 el.style.borderColor = this.hexToRgba(borderColor, Math.min(opacity + 0.15, 0.45)); 445 } 446 } else { 447 el.style.backgroundImage = `linear-gradient(${direction}, ${gradientDark}, ${gradientLight})`; 448 if (border) { 449 el.style.borderColor = borderColor; 450 } 451 } 452 }, 453 clearGradientClass(el) { 454 el.classList.remove('dynamic-gradient'); 455 el.style.backgroundImage = ''; 456 el.style.borderColor = ''; 457 } 458 })); 459 460 window.alpineFunctions.scrollOnClick = function () { 461 return { 462 scrollToId(id, offset = 0) { 463 const el = document.getElementById(id); 464 if (!el) return; 465 const y = el.getBoundingClientRect().top + window.scrollY - offset; 466 window.scrollTo({ top: y, behavior: 'smooth' }); 467 }, 468 init() { 469 const raw = window.location.search; 470 const safeSearch = encodeURI(raw); 471 const params = new URLSearchParams(safeSearch); 472 if (params.has('prompt_claim')) { 473 setTimeout(() => { 474 this.scrollToId('verify-player', 300); 475 }, 500); 476 } 477 } 478 } 479 }; 480 481 /** 482 * My Events page â subscriber benefit banner and upcoming events list. 483 * 484 * @param {{ apiUrl?: string, browseAllUrl?: string, admissionCode?: string }} config 485 */ 486 window.alpineFunctions.myEventsPage = function (config = {}) { 487 return { 488 apiUrl: config.apiUrl || '', 489 browseAllUrl: config.browseAllUrl || '#', 490 loading: false, 491 error: null, 492 events: [], 493 admissionCode: config.admissionCode || '', 494 copyLabel: 'Copy', 495 copyTimeout: null, 496 497 init() { 498 this.fetchEvents(); 499 }, 500 501 mapApiEvent(event) { 502 const start = new Date(event.start_date); 503 const end = new Date(event.end_date); 504 505 return { 506 id: event.id, 507 month: start.toLocaleString('en-US', { month: 'short' }), 508 days: this.formatEventDays(start, end), 509 name: event.title, 510 location: event.location || '', 511 badge: this.getEventBadge(start), 512 ticketUrl: this.browseAllUrl + '&event_id=' + event.id + '&coupon=' + this.admissionCode, 513 }; 514 }, 515 516 formatEventDays(start, end) { 517 const startDay = start.getDate(); 518 const endDay = end.getDate(); 519 const sameMonth = 520 start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear(); 521 522 if (sameMonth && startDay !== endDay) { 523 return `${startDay}-${endDay}`; 524 } 525 526 if (startDay === endDay && sameMonth) { 527 return String(startDay); 528 } 529
530 const startLabel = start.toLocaleString('en-US', { month: 'short', day: 'numeric' }); 531 const endLabel = end.toLocaleString('en-US', { month: 'short', day: 'numeric' }); 532 return `${startLabel}-${endLabel}`; 533 }, 534 535 getEventBadge(start) { 536 const now = new Date(); 537 now.setHours(0, 0, 0, 0); 538 539 const monday = new Date(now); 540 const day = now.getDay(); 541 monday.setDate(now.getDate() - ((day + 6) % 7)); 542 543 const friday = new Date(monday); 544 friday.setDate(monday.getDate() + 4); 545 friday.setHours(0, 0, 0, 0); 546 547 const sunday = new Date(friday); 548 sunday.setDate(friday.getDate() + 2); 549 sunday.setHours(23, 59, 59, 999); 550 551 const startDay = new Date(start); 552 startDay.setHours(0, 0, 0, 0); 553 554 if (startDay >= friday && startDay <= sunday) { 555 return 'This weekend'; 556 } 557 558 return null; 559 }, 560 561 async fetchEvents() { 562 if (!this.apiUrl) { 563 this.error = 'Events API is not configured.'; 564 return; 565 } 566 567 this.loading = true; 568 this.error = null; 569 570 try { 571 const response = await fetch(this.apiUrl); 572 573 if (!response.ok) { 574 throw new Error('Failed to load events'); 575 } 576 577 const data = await response.json(); 578 this.events = Array.isArray(data) ? data.map((event) => this.mapApiEvent(event)) : []; 579 } catch (err) { 580 this.error = err?.message || 'Unable to load events. Please try again.'; 581 this.events = []; 582 } finally { 583 this.loading = false; 584 } 585 }, 586 587 async copyToClipboard(text = null) { 588 if (!text) { 589 return; 590 } 591 592 var success = false; 593 594 try { 595 await navigator.clipboard.writeText(text); 596 success = true; 597 } catch { 598 // Fallback for browsers without clipboard API 599 const input = document.createElement('textarea'); 600 input.value = text; 601 input.setAttribute('readonly', ''); 602 input.style.position = 'absolute'; 603 input.style.left = '-9999px'; 604 document.body.appendChild(input); 605 input.select(); 606 document.execCommand('copy'); 607 document.body.removeChild(input); 608 } 609 if (success) { 610 const el = this.$el; 611 const svgNs = 'http://www.w3.org/2000/svg'; 612 const iconEl = document.createElementNS(svgNs, 'svg'); 613 614 iconEl.setAttribute('class', 'w-4 h-4 text-green-500'); 615 iconEl.setAttribute('fill', 'currentColor'); 616 iconEl.setAttribute('viewBox', '0 0 20 20'); 617 618 const path = document.createElementNS(svgNs, 'path'); 619 620 path.setAttribute('fill-rule', 'evenodd'); 621 path.setAttribute( 622 'd', 623 'M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z', 624 ); 625 path.setAttribute('clip-rule', 'evenodd'); 626 iconEl.appendChild(path); 627 628 el.classList.add('hidden'); 629 el.insertAdjacentElement('afterend', iconEl); 630 631 setTimeout(() => { 632 iconEl.remove(); 633 el.classList.remove('hidden'); 634 }, 2000); 635 } 636 }, 637 }; 638 }; 639 640 // Example: Add more components here 641 // window.alpineFunctions.anotherComponent = function () { 642 // return { /* ... */ }; 643 // }; 644}); 645; 646/** 647 * Hero Section Video Modal Component 648 */ 649(function() { 650 const registerComponent = () => { 651 window.alpineFunctions = window.alpineFunctions || {}; 652 653 window.alpineFunctions.heroVideoModal = function () { 654 return { 655 showVideo: false, 656 modalId: 'hero-video-modal-' + Math.random().toString(36).slice(2, 11), 657 658 openModal() { 659 this.showVideo = true; 660 document.body.classList.add('overflow-hidden'); 661 662 // Play video after modal opens 663 this.$nextTick(() => { 664 const modal = document.getElementById(this.modalId); 665 if (!modal) return; 666 667 const video = modal.querySelector('video'); 668 const iframe = modal.querySelector('iframe'); 669 670 if (video) { 671 // Load and play the video 672 video.load(); 673 674 // Give the video a moment to start loading 675 setTimeout(() => { 676 video.currentTime = 0; 677 video.play().catch(() => {
678 // If autoplay fails, controls are visible for manual play 679 }); 680 }, 300); 681 } else if (iframe && iframe.src) { 682 // For YouTube, ensure autoplay with sound 683 if (iframe.src.includes('youtube.com')) { 684 let newSrc = iframe.src; 685 if (!newSrc.includes('autoplay=')) { 686 newSrc += (newSrc.includes('?') ? '&' : '?') + 'autoplay=1'; 687 } 688 if (newSrc.includes('mute=1')) { 689 newSrc = newSrc.replace('mute=1', 'mute=0'); 690 } else if (!newSrc.includes('mute=')) { 691 newSrc += '&mute=0'; 692 } 693 iframe.src = newSrc; 694 } 695 } 696 }); 697 }, 698 699 closeModal() { 700 const modal = document.getElementById(this.modalId); 701 702 if (modal) { 703 // Stop all videos 704 modal.querySelectorAll('video').forEach(v => { 705 v.pause(); 706 v.currentTime = 0; 707 }); 708 709 // Reset iframes 710 modal.querySelectorAll('iframe').forEach(iframe => { 711 const originalSrc = iframe.src; 712 iframe.src = 'about:blank'; 713 setTimeout(() => { 714 iframe.src = originalSrc.replace(/[?&](autoplay|mute)=[^&]*/g, ''); 715 }, 300); 716 }); 717 } 718 719 this.showVideo = false; 720 document.body.classList.remove('overflow-hidden'); 721 }, 722 723 init() { 724 // Listen for ESC key 725 this.$watch('showVideo', value => { 726 if (value) { 727 const escHandler = (e) => { 728 if (e.key === 'Escape') { 729 this.closeModal(); 730 document.removeEventListener('keydown', escHandler); 731 } 732 }; 733 document.addEventListener('keydown', escHandler); 734 } 735 }); 736 } 737 }; 738 }; 739 }; 740 741 // Register on alpine:init to ensure Alpine is loaded 742 document.addEventListener('alpine:init', registerComponent); 743})();; 744// Global Alpine.js functions 745(function() { 746 const registerGlobalFunctions = () => { 747 window.alpineFunctions = window.alpineFunctions || {}; 748 window.alpineFunctions.backgroundImageComponent = function (srcset = '', fallback = '') { 749 return { 750 srcset, 751 fallback, 752 currentSrc: fallback, 753 _debounceTimeout: null, 754 755 init() { 756 this.update(); 757 window.addEventListener('resize', this.debouncedUpdate.bind(this)); 758 }, 759 update() { 760 if (!this.srcset) { 761 return; 762 } 763 const sources = Array.from(this.srcset.matchAll(/(https?:\/\/.+?)(?=\s+\d+w)/g)).map((url, i, arr) => { 764 const widths = [...this.srcset.matchAll(/(\d+)w/g)]; 765 return { 766 url: url[1], 767 width: parseInt(widths[i][1]), 768 }; 769 }).filter(source => source.width !== source.height); 770 sources.sort((a, b) => a.width - b.width); 771 const viewport = window.innerWidth; 772 let selected = sources[sources.length - 1]?.url || this.fallback; 773 for (const source of sources) { 774 if (viewport <= source.width) { 775 selected = source.url; 776 break; 777 } 778 } 779 this.currentSrc = selected; 780 }, 781 debouncedUpdate() { 782 clearTimeout(this._debounceTimeout); 783 this._debounceTimeout = setTimeout(() => { 784 this.update(); 785 }, 250); 786 } 787 }; 788 }; 789 790 /** 791 * Color Utils 792 * Global helper functions for working with colors 793 * - createGradientClass: Creates a gradient class for an element 794 * - rotateHueAndLighten: Rotates a hue and lightens a color 795 * - darken: Darkens a color 796 * 797 * Gradient usage: 798 * <div x-data x-init=" 799 * $el.style.setProperty('--primary', $colorUtils.getVar('--site-primary')); 800 * $colorUtils.createGradientClass($el, 350, 44); 801 * "> 802 * $el is the element that the gradient class is being applied to 803 * --primary is the variable that is being set to the site primary color 804 * 350 is the hue offset 805 * 44 is the lighten amount 806 * 807 */ 808 Alpine.magic('colorUtils', () => ({ 809 getVar(name) { 810 return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); 811 }, 812 /** 813 * hexToHsl: Converts a hex color to an hsl color 814 */ 815 hexToHsl(hex) { 816 hex = hex.replace('#', '');
817 let r = parseInt(hex.slice(0, 2), 16) / 255; 818 let g = parseInt(hex.slice(2, 4), 16) / 255; 819 let b = parseInt(hex.slice(4, 6), 16) / 255; 820 821 let max = Math.max(r, g, b), min = Math.min(r, g, b); 822 let h, s, l = (max + min) / 2; 823 824 if (max === min) { 825 h = s = 0; 826 } else { 827 let d = max - min; 828 s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 829 switch (max) { 830 case r: h = (g - b) / d + (g < b ? 6 : 0); break; 831 case g: h = (b - r) / d + 2; break; 832 case b: h = (r - g) / d + 4; break; 833 } 834 h /= 6; 835 } 836 837 return { 838 h: Math.round(h * 360), 839 s: Math.round(s * 100), 840 l: Math.round(l * 100), 841 }; 842 }, 843 844 /** 845 * hslToHex: Converts an hsl color to a hex color 846 */ 847 hslToHex(h, s, l) { 848 s /= 100; 849 l /= 100; 850 851 const c = (1 - Math.abs(2 * l - 1)) * s; 852 const x = c * (1 - Math.abs((h / 60) % 2 - 1)); 853 const m = l - c / 2; 854 855 let r, g, b; 856 if (h < 60) [r, g, b] = [c, x, 0]; 857 else if (h < 120) [r, g, b] = [x, c, 0]; 858 else if (h < 180) [r, g, b] = [0, c, x]; 859 else if (h < 240) [r, g, b] = [0, x, c]; 860 else if (h < 300) [r, g, b] = [x, 0, c]; 861 else [r, g, b] = [c, 0, x]; 862 863 r = Math.round((r + m) * 255); 864 g = Math.round((g + m) * 255); 865 b = Math.round((b + m) * 255); 866 867 return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`; 868 }, 869 /** 870 * rotateHueAndLighten: Rotates a hue and lightens a color 871 * this is based on wheel of colors so we match the right contrast for the color 872 * https://www.rapidtables.com/web/color/RGB_Color.html 873 * 0 is the starting point of the wheel of colors based on the color passed in 874 * 360 is the ending point of the wheel of colors 875 */ 876 rotateHueAndLighten(hex, hueOffset = 30, lightenAmount = 30) { 877 const hsl = this.hexToHsl(hex); 878 const newHue = (hsl.h + hueOffset) % 360; 879 /**Exeptions prep dig / redzone **/ 880 /** those colors were too dark so we lighten them up a bit manually */ 881 const exceptions = {213: 57, 357: 54}; 882 if(Object.keys(exceptions).includes(hsl.h.toString())) { 883 lightenAmount = exceptions[hsl.h.toString()]; 884 } 885 const newLightness = Math.min(100, hsl.l + lightenAmount); 886 return this.hslToHex(newHue, hsl.s, newLightness); 887 }, 888 /**
889 * darken: Darkens a color 890 */ 891 darken(hex, percent) { 892 hex = hex.replace('#', ''); 893 let r = parseInt(hex.substring(0, 2), 16); 894 let g = parseInt(hex.substring(2, 4), 16); 895 let b = parseInt(hex.substring(4, 6), 16); 896 897 r = Math.max(0, Math.floor(r * (1 - percent / 100))); 898 g = Math.max(0, Math.floor(g * (1 - percent / 100))); 899 b = Math.max(0, Math.floor(b * (1 - percent / 100))); 900 901 return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`; 902 }, 903 /** 904 * hexToRgba: Converts a hex color to rgba 905 */ 906 hexToRgba(hex, alpha) { 907 hex = hex.replace('#', ''); 908 if (hex.length === 3) { 909 hex = hex.split('').map((char) => char + char).join(''); 910 } 911 912 const r = parseInt(hex.slice(0, 2), 16); 913 const g = parseInt(hex.slice(2, 4), 16); 914 const b = parseInt(hex.slice(4, 6), 16); 915 916 return `rgba(${r}, ${g}, ${b}, ${alpha})`; 917 }, 918 /** 919 * createGradientClass: Creates a gradient class for an element 920 * 921 * @param {HTMLElement} el 922 * @param {number} hueOffset 923 * @param {number} lightenAmount 924 * @param {boolean} border 925 * @param {string} direction CSS linear-gradient direction (e.g. 'to right', 'to top') 926 * @param {number|null} opacity Optional alpha for a lighter transparent wash (0â1) 927 */ 928 createGradientClass(el, hueOffset = 30, lightenAmount = 30, border = true, direction = 'to right', opacity = null) { 929 const primary = this.getVar('--site-primary'); 930 const gradientDark = this.rotateHueAndLighten(primary, hueOffset+30, lightenAmount); 931 const gradientLight = this.rotateHueAndLighten(primary, hueOffset, lightenAmount-5); 932 const borderColor = this.rotateHueAndLighten(primary, 0, lightenAmount-3); 933 934 el.classList.add('dynamic-gradient'); 935 936 if (opacity !== null) { 937 el.style.backgroundImage = `linear-gradient(${direction}, ${this.hexToRgba(gradientDark, opacity)}, ${this.hexToRgba(gradientLight, opacity * 0.25)})`; 938 if (border) { 939 el.style.borderColor = this.hexToRgba(borderColor, Math.min(opacity + 0.15, 0.45)); 940 } 941 } else { 942 el.style.backgroundImage = `linear-gradient(${direction}, ${gradientDark}, ${gradientLight})`; 943 if (border) { 944 el.style.borderColor = borderColor; 945 } 946 } 947 }, 948 clearGradientClass(el) { 949 el.classList.remove('dynamic-gradient'); 950 el.style.backgroundImage = ''; 951 el.style.borderColor = ''; 952 } 953 })); 954 955 /** 956 * Autocomplete Component - Initialization wrapper 957 * Reads searchTypes from data attribute to avoid JSON encoding issues 958 */ 959 Alpine.data('autocompleteDataInit', (elementId, selectType, fieldToApply, inputId) => { 960 // Create a completely fresh instance each time 961 // We'll read searchTypes in init(), so start with empty array 962 return autocompleteDataFunction([], selectType, fieldToApply, elementId, inputId); 963 }); 964 965 /** 966 * Autocomplete Component - Core function 967 * Global Alpine.js data function for autocomplete search inputs 968 */ 969 function autocompleteDataFunction(searchTypes, selectType, fieldToApply, elementId, inputId) { 970 // Ensure searchTypes is a new array to avoid reference sharing 971 const searchTypesArray = Array.isArray(searchTypes) ? [...searchTypes] : []; 972 973 return { 974 searchText: '', 975 searchTypes: searchTypesArray, 976 get isPlayerSearch() { 977 return Array.isArray(this.searchTypes) && this.searchTypes.includes('players'); 978 }, 979 errorMessage: '', 980 isLoading: false, 981 showResults: false, 982 resultsHtml: '', 983 selectedItemId: null, 984 timer: null, 985 clickHandlerAdded: false, // Track if click handler has been added 986 get url() { 987 return (typeof ajax_object !== 'undefined' && ajax_object?.ajax_url) 988 ? ajax_object.ajax_url 989 : '/wp-admin/admin-ajax.php'; 990 }, 991 fieldToApply: fieldToApply || '', 992 selectType: selectType || 'id', 993 elementId: elementId || '', 994 inputId: inputId || '', 995 996 init() { 997 // Always read searchTypes from data attribute (it's the source of truth) 998 const element = document.getElementById(this.elementId); 999 if (element) { 1000 try { 1001 const dataAttr = element.getAttribute('data-search-types'); 1002 if (dataAttr) { 1003 const parsed = JSON.parse(dataAttr); 1004 if (Array.isArray(parsed)) { 1005 this.searchTypes = parsed; 1006 } 1007 } 1008 } catch (e) { 1009 console.error('Error parsing searchTypes from data attribute:', e); 1010 } 1011 } 1012 1013 // Set initial value from input if it exists (but don't use value attribute to avoid conflicts with x-model) 1014 this.$nextTick(() => { 1015 const initialInput = document.getElementById(this.inputId); 1016 if (initialInput && initialInput.hasAttribute('value') && initialInput.getAttribute('value')) { 1017 this.searchText = initialInput.getAttribute('value'); 1018 } 1019 }); 1020 1021 if (this.selectType === 'id') { 1022 // Use event delegation on the results list to handle clicks 1023 this.$watch('resultsHtml', () => { 1024 this.$nextTick(() => { 1025 setTimeout(() => { 1026 const resultsList = document.querySelector(`#${this.elementId} ul#options`); 1027 if (resultsList) { 1028 // Initialize Alpine.js on the results list 1029 if (typeof window.Alpine !== 'undefined' && window.Alpine.initTree) { 1030 window.Alpine.initTree(resultsList); 1031 } 1032 1033 // Add click handler using event delegation (only once per component instance) 1034 if (!this.clickHandlerAdded) { 1035 resultsList.addEventListener('click', (e) => { 1036 const li = e.target.closest('li'); 1037 if (li) { 1038 e.preventDefault(); 1039 e.stopPropagation();
1040 this.handleItemClick(li); 1041 } 1042 }); 1043 this.clickHandlerAdded = true; 1044 } 1045 } 1046 }, 50); 1047 }); 1048 }); 1049 } 1050 }, 1051 1052 handleInput() { 1053 if (this.isPlayerSearch || this.searchText.length === 0) { 1054 this.errorMessage = ''; 1055 } 1056 this.performSearch(); 1057 }, 1058 1059 handleFocus() { 1060 if (this.isPlayerSearch) { 1061 this.errorMessage = ''; 1062 } 1063 if (this.resultsHtml && this.resultsHtml.trim() !== '') { 1064 this.showResults = true; 1065 } 1066 }, 1067 1068 handleKeyup() { 1069 if (this.isPlayerSearch) { 1070 this.errorMessage = ''; 1071 } 1072 }, 1073 1074 handlePaste() { 1075 if (this.isPlayerSearch) { 1076 this.errorMessage = ''; 1077 } 1078 }, 1079 1080 handleBlur() { 1081 if (this.isPlayerSearch && this.searchText.length === 0) { 1082 this.errorMessage = ''; 1083 } 1084 // Hide results when input loses focus (with a small delay to allow clicks on results) 1085 setTimeout(() => { 1086 // Only hide if focus hasn't moved to the results list 1087 const resultsList = document.querySelector(`#${this.elementId} ul#options`); 1088 const activeElement = document.activeElement; 1089 if (resultsList && !resultsList.contains(activeElement)) { 1090 this.showResults = false; 1091 } 1092 }, 200); 1093 }, 1094 1095 handleClick() { 1096 if (this.resultsHtml && this.resultsHtml.trim() !== '') { 1097 this.showResults = true; 1098 } 1099 }, 1100 1101 performSearch() { 1102 if (this.isPlayerSearch) { 1103 this.errorMessage = ''; 1104 } 1105 1106 const searchLength = this.searchText.length; 1107 1108 // For player searches, require at least 2 words (first and last name) 1109 if (this.isPlayerSearch) { 1110 const words = this.searchText.trim().split(/\s+/).filter(word => word.length > 0); 1111 if (words.length < 2 && this.searchText.trim().length > 0) { 1112 // Show error message if user has typed something but not enough words 1113 this.errorMessage = 'Please enter both first and last name to search for players.'; 1114 this.showResults = false; 1115 this.resultsHtml = ''; 1116 this.isLoading = false; 1117 if (this.fieldToApply && this.$refs.fieldToApply) { 1118 this.$refs.fieldToApply.value = ''; 1119 } 1120 return; 1121 } else if (words.length < 2) { 1122 // Field is empty or only whitespace - clear error and don't search 1123 this.showResults = false; 1124 this.resultsHtml = ''; 1125 this.errorMessage = ''; 1126 this.isLoading = false; 1127 if (this.fieldToApply && this.$refs.fieldToApply) { 1128 this.$refs.fieldToApply.value = ''; 1129 } 1130 return; 1131 } 1132 } 1133 1134 // For non-player searches, require at least 3 characters 1135 if (!this.isPlayerSearch && searchLength < 3) { 1136 this.errorMessage = 'Please enter at least three letters when searching'; 1137 this.showResults = false; 1138 if (this.fieldToApply && this.$refs.fieldToApply) { 1139 this.$refs.fieldToApply.value = ''; 1140 } 1141 return; 1142 } 1143 1144 this.errorMessage = ''; 1145 this.isLoading = true; 1146 this.resultsHtml = ''; 1147 1148 // Ensure URL is set correctly 1149 const ajaxUrl = (typeof ajax_object !== 'undefined' && ajax_object?.ajax_url) 1150 ? ajax_object.ajax_url 1151 : '/wp-admin/admin-ajax.php'; 1152 1153 fetch(ajaxUrl, { 1154 method: 'POST', 1155 headers: { 1156 'Content-Type': 'application/x-www-form-urlencoded', 1157 }, 1158 credentials: 'same-origin', 1159 body: new URLSearchParams({ 1160 itemName: this.searchText, 1161 postTypes: JSON.stringify(this.searchTypes), 1162 selectType: this.selectType, 1163 action: 'get_items_for_autocomplete' 1164 }) 1165 }) 1166 .then(response => { 1167 if (!response.ok) { 1168 throw new Error(`HTTP error! status: ${response.status}`); 1169 } 1170 return response.text(); 1171 }) 1172 .then(html => { 1173 this.isLoading = false; 1174 const hasResults = html.trim() !== ''; 1175 1176 if (hasResults) { 1177 // Set HTML first 1178 this.resultsHtml = html; 1179 1180 // Wait for DOM to update, then show and initialize Alpine 1181 this.$nextTick(() => { 1182 // Set showResults after DOM is updated 1183 this.showResults = true; 1184 1185 // Explicitly remove hidden class if it exists 1186 setTimeout(() => { 1187 const resultsList = document.querySelector(`#${this.elementId} ul#options`); 1188 if (resultsList) { 1189 resultsList.classList.remove('hidden'); 1190 1191 // Initialize Alpine.js on the inserted HTML 1192 if (typeof window.Alpine !== 'undefined' && window.Alpine.initTree) { 1193 try { 1194 window.Alpine.initTree(resultsList); 1195 } catch (e) { 1196 console.error('Error initializing Alpine on results:', e); 1197 } 1198 } 1199 } 1200 }, 50); 1201 }); 1202 } else { 1203 this.resultsHtml = ''; 1204 this.showResults = false;
1205 } 1206 }) 1207 .catch(error => { 1208 this.isLoading = false; 1209 this.errorMessage = 'An error occurred while searching. Please try again.'; 1210 this.showResults = false; 1211 console.error('Autocomplete error:', error); 1212 console.error('Search text:', this.searchText); 1213 console.error('Search types:', this.searchTypes); 1214 console.error('AJAX URL:', ajaxUrl); 1215 }); 1216 }, 1217 1218 handleItemClick(li) { 1219 const isChecked = li.classList.contains('checked'); 1220 const allItems = document.querySelectorAll(`#${this.elementId} li`); 1221 1222 if (isChecked) { 1223 li.classList.remove('checked'); 1224 const checkIcon = li.querySelector('.item-check'); 1225 if (checkIcon) checkIcon.classList.add('hidden'); 1226 this.searchText = ''; 1227 if (this.fieldToApply && this.$refs.fieldToApply) { 1228 this.$refs.fieldToApply.value = ''; 1229 } 1230 } else { 1231 allItems.forEach(item => { 1232 item.classList.remove('checked'); 1233 const checkIcon = item.querySelector('.item-check'); 1234 if (checkIcon) checkIcon.classList.add('hidden'); 1235 }); 1236 1237 li.classList.add('checked'); 1238 const checkIcon = li.querySelector('.item-check'); 1239 if (checkIcon) checkIcon.classList.remove('hidden'); 1240 1241 const itemName = li.querySelector('.item-name'); 1242 if (itemName) { 1243 this.searchText = itemName.textContent.trim(); 1244 } 1245 1246 this.showResults = false; 1247 1248 if (this.fieldToApply && this.$refs.fieldToApply) { 1249 const itemWrapper = li.querySelector('.item-wrapper'); 1250 if (itemWrapper && itemWrapper.dataset && itemWrapper.dataset.id) { 1251 this.$refs.fieldToApply.value = itemWrapper.dataset.id; 1252 } else if (itemWrapper && itemWrapper.getAttribute('data-id')) { 1253 this.$refs.fieldToApply.value = itemWrapper.getAttribute('data-id'); 1254 } 1255 } 1256 } 1257 } 1258 }; 1259 } 1260 1261 // Also register as a direct function for backwards compatibility 1262 Alpine.data('autocompleteData', (searchTypes, selectType, fieldToApply, elementId, inputId) => { 1263 return autocompleteDataFunction(searchTypes, selectType, fieldToApply, elementId, inputId); 1264 }); 1265 }; 1266 1267 // Register immediately if Alpine is available, otherwise wait for alpine:init 1268 if (typeof window.Alpine !== 'undefined') { 1269 registerGlobalFunctions(); 1270 } else { 1271 document.addEventListener('alpine:init', registerGlobalFunctions); 1272 } 1273})(); 1274 1275;
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.