PageSourceSearch

https://prepdig.com/_static/??-eJytjrEOwjAMRH+I4CAEiAGx8R9pZGjaxI5…/iC0WwytzF3JD3cvtcLn6sz/6kx8+pKCxFg==

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.