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