PageSourceSearch

https://prephoops.com/_static/??-eJytjrEOwjAMRH+IYBCVSgfExn+kkaFpE…/ZYDK/EvU8Vdc+3c9ud2uZy7ZrxA6TzsSs=

js prephoops.com collected 2026-10-01 10:41:44 UTC 44,770 bytes, 1,363 lines download raw bytes

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.