PageSourceSearch

https://prepredzone.com/_static/??-eJylzEEOQEAMQNELodhgIXbuwaRhGG3…/Z7AlXpeUV9M1Nt1Qt21uf7zJEz4/

js prepredzone.com collected 2026-09-26 06:14:36 UTC 33,039 bytes, 1,039 lines download raw bytes

1// In alpineFunctions.js
2/**
3 * Alpine Functions
4 * This file contains the Alpine functions for the Prep Network Content theme
5 */
6document.addEventListener('alpine:init', () => {
7	window.alpineFunctions = window.alpineFunctions || {};
8	/**
9	 * Background Image Component
10	 * This component is used to load the background image
11	 * It uses the srcset to load the image and the fallback to load the image if the srcset is not available
12	 * It also uses the debouncedUpdate to update the image when the window is resized
13	 */
14	window.alpineFunctions.backgroundImageComponent = function (srcset = '', fallback = '') {
15		return {
16			srcset,
17			fallback,
18			currentSrc: fallback,
19			_debounceTimeout: null,
20
21			init() {
22				this.update();
23				window.addEventListener('resize', this.debouncedUpdate.bind(this));
24			},
25			update() {
26				if (!this.srcset) {
27					return;
28				}
29				const sources = Array.from(this.srcset.matchAll(/(https?:\/\/.+?)(?=\s+\d+w)/g)).map((url, i, arr) => {
30					const widths = [...this.srcset.matchAll(/(\d+)w/g)];
31					return {
32						url: url[1],
33						width: parseInt(widths[i][1]),
34					};
35				}).filter(source => source.width !== source.height);
36				sources.sort((a, b) => a.width - b.width);
37				const viewport = window.innerWidth;			
38				let selected = sources[sources.length - 1]?.url || this.fallback;			
39				for (const source of sources) {
40					if (viewport <= source.width) {
41						selected = source.url;
42						break;
43					}
44				}
45				this.currentSrc = selected;
46			},
47			debouncedUpdate() {
48				clearTimeout(this._debounceTimeout);
49				this._debounceTimeout = setTimeout(() => {
50					this.update();
51				}, 250);
52			}
53		};
54	};
55
56	/**
57	 * Lazy Background Component
58	 * This component is used to lazy load the background image
59	 * It uses the Intersection Observer API to load the image when it is in view
60	 * It also uses the backgroundImageComponent to handle the image loading and resizing
61	 */
62	Alpine.data('lazyBackgroundComponent', (srcset, fallback) => ({
63        observer: null,
64        loaded: false,
65        child: null, // this will hold the backgroundImageComponent instance
66        resizeHandler: null, // store handler reference for cleanup
67        syncTimeout: null, // store timeout reference for cleanup
68        isSliderSlide: false, // track if this is a slider slide
69
70        currentSrc: '', // exposed for Blade x-bind
71
72        init() {
73            // Check if element has data-slide-index attribute (slider slides)
74            this.isSliderSlide = this.$el.hasAttribute('data-slide-index');
75            
76            if (this.isSliderSlide) {
77                // For slider slides, set currentSrc to empty initially
78                // They will be loaded manually via Slick events
79                this.currentSrc = '';
80                // Don't create IntersectionObserver - will be loaded manually
81                return;
82            }
83
84            // For non-slider elements, use fallback and IntersectionObserver
85            this.currentSrc = fallback;
86
87            this.observer = new IntersectionObserver((entries) => {
88                if (entries[0].isIntersecting) {
89                    this.loadBackground();
90                    this.observer.disconnect();
91                }
92            }, {
93                rootMargin: '200px', // preload slightly before visibility
94            });
95
96            this.observer.observe(this.$el);
97        },
98
99        loadBackground() {
100            if (this.loaded) return;
101            this.loaded = true;
102
103            // initialize the responsive component dynamically
104            this.child = window.alpineFunctions.backgroundImageComponent(srcset, fallback);
105
106            // Manually run its init + update routine
107            this.child.init();
108            this.child.update();
109
110            // Sync the background to the wrapper value
111            this.currentSrc = this.child.currentSrc;
112
113            // Preload the image to improve perceived load time
114            if (this.currentSrc) {
115                const img = new Image();
116                img.src = this.currentSrc;
117            }
118
119            // Keep them in sync on resize
120            // Store handler reference for cleanup
121            this.resizeHandler = () => {
122                if (!this.child) return; // safety check
123                this.child.debouncedUpdate();
124                // Clear any pending sync timeout
125                if (this.syncTimeout) {
126                    clearTimeout(this.syncTimeout);
127                }
128                // Sync after debounce completes (250ms delay)
129                this.syncTimeout = setTimeout(() => {
130                    if (this.child) {
131                        this.currentSrc = this.child.currentSrc;
132                    }
133                    this.syncTimeout = null;
134                }, 250);
135            };
136            window.addEventListener('resize', this.resizeHandler);
137        },
138
139        destroy() {
140            // Cleanup: remove resize listener, clear timeouts, and disconnect observer
141            if (this.resizeHandler) {
142                window.removeEventListener('resize', this.resizeHandler);
143                this.resizeHandler = null;
144            }
145            if (this.syncTimeout) {
146                clearTimeout(this.syncTimeout);
147                this.syncTimeout = null;
148            }
149            if (this.observer) {
150                this.observer.disconnect();
151                this.observer = null;
152            }
153        }
154    }));
155
156	/**
157	 * Color Utils
158	 * Global helper functions for working with colors
159	 * - createGradientClass: Creates a gradient class for an element
160	 * - clearGradientClass: Removes a dynamic gradient from an element
161	 * - rotateHueAndLighten: Rotates a hue and lightens a color
162	 * - darken: Darkens a color
163	 * 
164	 * Gradient usage:
165	 * <div x-data x-init="
166	 *     $el.style.setProperty('--primary', $colorUtils.getVar('--site-primary'));
167	 *     $colorUtils.createGradientClass($el, 350, 44);
168	 * ">
169	 * $el is the element that the gradient class is being applied to
170	 * --primary is the variable that is being set to the site primary color
171	 * 350 is the hue offset
172	 * 44 is the lighten amount
173	 * 
174	 */
175	Alpine.magic('colorUtils', () => ({
176		getVar(name) {
177			return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
178		},
179		/**
180		 * hexToHsl: Converts a hex color to an hsl color
181		 */
182		hexToHsl(hex) {
183			hex = hex.replace('#', '');
184			let r = parseInt(hex.slice(0, 2), 16) / 255;
185			let g = parseInt(hex.slice(2, 4), 16) / 255;
186			let b = parseInt(hex.slice(4, 6), 16) / 255;
187
188			let max = Math.max(r, g, b), min = Math.min(r, g, b);
189			let h, s, l = (max + min) / 2;
190
191			if (max === min) {
192				h = s = 0;
193			} else {
194				let d = max - min;
195				s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
196				switch (max) {
197					case r: h = (g - b) / d + (g < b ? 6 : 0); break;
198					case g: h = (b - r) / d + 2; break;
199					case b: h = (r - g) / d + 4; break;
200				}
201				h /= 6;
202			}
203
204			return {
205				h: Math.round(h * 360),
206				s: Math.round(s * 100),
207				l: Math.round(l * 100),
208			};
209		},
210
211		/**
212		 * hslToHex: Converts an hsl color to a hex color
213		 */
214		hslToHex(h, s, l) {
215			s /= 100;
216			l /= 100;
217
218			const c = (1 - Math.abs(2 * l - 1)) * s;
219			const x = c * (1 - Math.abs((h / 60) % 2 - 1));
220			const m = l - c / 2;
221
222			let r, g, b;
223			if (h < 60) [r, g, b] = [c, x, 0];
224			else if (h < 120) [r, g, b] = [x, c, 0];
225			else if (h < 180) [r, g, b] = [0, c, x];
226			else if (h < 240) [r, g, b] = [0, x, c];
227			else if (h < 300) [r, g, b] = [x, 0, c];
228			else [r, g, b] = [c, 0, x];
229
230			r = Math.round((r + m) * 255);
231			g = Math.round((g + m) * 255);
232			b = Math.round((b + m) * 255);
233
234			return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`;
235		},
236		/**
237		 * rotateHueAndLighten: Rotates a hue and lightens a color
238		 * this is based on wheel of colors so we match the right contrast for the color
239		 * https://www.rapidtables.com/web/color/RGB_Color.html
240		 * 0 is the starting point of the wheel of colors based on the color passed in
241		 * 360 is the ending point of the wheel of colors
242		 */
243		rotateHueAndLighten(hex, hueOffset = 30, lightenAmount = 30) {
244			const hsl = this.hexToHsl(hex);
245			const newHue = (hsl.h + hueOffset) % 360;
246			/**Exeptions prep dig / redzone **/
247			/** those colors were too dark so we lighten them up a bit manually */
248			const exceptions = {213: 57, 357: 54};
249			if(Object.keys(exceptions).includes(hsl.h.toString())) {
250				lightenAmount = exceptions[hsl.h.toString()];
251			} 
252			const newLightness = Math.min(100, hsl.l + lightenAmount);
253			return this.hslToHex(newHue, hsl.s, newLightness);
254		},
255		/**
256		 * darken: Darkens a color
257		 */
258		darken(hex, percent) {
259			hex = hex.replace('#', '');
260			let r = parseInt(hex.substring(0, 2), 16);
261			let g = parseInt(hex.substring(2, 4), 16);
262			let b = parseInt(hex.substring(4, 6), 16);
263		
264			r = Math.max(0, Math.floor(r * (1 - percent / 100)));
265			g = Math.max(0, Math.floor(g * (1 - percent / 100)));
266			b = Math.max(0, Math.floor(b * (1 - percent / 100)));
267		
268			return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`;
269		},
270		/**
271		 * hexToRgba: Converts a hex color to rgba
272		 */
273		hexToRgba(hex, alpha) {
274			hex = hex.replace('#', '');
275			if (hex.length === 3) {
276				hex = hex.split('').map((char) => char + char).join('');
277			}
278
279			const r = parseInt(hex.slice(0, 2), 16);
280			const g = parseInt(hex.slice(2, 4), 16);
281			const b = parseInt(hex.slice(4, 6), 16);
282
283			return `rgba(${r}, ${g}, ${b}, ${alpha})`;
284		},
285		/**
286		 * createGradientClass: Creates a gradient class for an element
287		 *
288		 * @param {HTMLElement} el
289		 * @param {number} hueOffset
290		 * @param {number} lightenAmount
291		 * @param {boolean} border
292		 * @param {string} direction CSS linear-gradient direction (e.g. 'to right', 'to top')
293		 * @param {number|null} opacity Optional alpha for a lighter transparent wash (0–1)
294		 */
295		createGradientClass(el, hueOffset = 30, lightenAmount = 30, border = true, direction = 'to right', opacity = null) {
296			const primary = this.getVar('--site-primary');
297			const gradientDark = this.rotateHueAndLighten(primary, hueOffset+30, lightenAmount);
298			const gradientLight = this.rotateHueAndLighten(primary, hueOffset, lightenAmount-5);
299			const borderColor = this.rotateHueAndLighten(primary, 0, lightenAmount-3);
300
301			el.classList.add('dynamic-gradient');
302
303			if (opacity !== null) {
304				el.style.backgroundImage = `linear-gradient(${direction}, ${this.hexToRgba(gradientDark, opacity)}, ${this.hexToRgba(gradientLight, opacity * 0.25)})`;
305				if (border) {
306					el.style.borderColor = this.hexToRgba(borderColor, Math.min(opacity + 0.15, 0.45));
307				}
308			} else {
309				el.style.backgroundImage = `linear-gradient(${direction}, ${gradientDark}, ${gradientLight})`;
310				if (border) {
311					el.style.borderColor = borderColor;
312				}
313			}
314		},
315		clearGradientClass(el) {
316			el.classList.remove('dynamic-gradient');
317			el.style.backgroundImage = '';
318			el.style.borderColor = '';
319		}
320	}));
321
322	window.alpineFunctions.scrollOnClick = function () {
323		return {
324			scrollToId(id, offset = 0) {
325				const el = document.getElementById(id);
326				if (!el) return;
327				const y = el.getBoundingClientRect().top + window.scrollY - offset;
328				window.scrollTo({ top: y, behavior: 'smooth' });
329			},
330			init() {
331				const raw = window.location.search;
332				const safeSearch = encodeURI(raw);
333				const params = new URLSearchParams(safeSearch);
334				if (params.has('prompt_claim')) {
335				  setTimeout(() => {
336					this.scrollToId('verify-player', 300);
337				  }, 500);
338				}
339			}
340		}
341	};
342
343	/**
344	 * My Events page — subscriber benefit banner and upcoming events list.
345	 *
346	 * @param {{ apiUrl?: string, browseAllUrl?: string, admissionCode?: string }} config
347	 */
348	window.alpineFunctions.myEventsPage = function (config = {}) {
349		return {
350			apiUrl: config.apiUrl || '',
351			browseAllUrl: config.browseAllUrl || '#',
352			loading: false,
353			error: null,
354			events: [],
355			admissionCode: config.admissionCode || '',
356			copyLabel: 'Copy',
357			copyTimeout: null,
358
359			init() {
360				this.fetchEvents();
361			},
362
363			mapApiEvent(event) {
364				const start = new Date(event.start_date);
365				const end = new Date(event.end_date);
366
367				return {
368					id: event.id,
369					month: start.toLocaleString('en-US', { month: 'short' }),
370					days: this.formatEventDays(start, end),
371					name: event.title,
372					location: event.location || '',
373					badge: this.getEventBadge(start),
374					ticketUrl: this.browseAllUrl + '&event_id=' + event.id + '&coupon=' + this.admissionCode,
375				};
376			},
377
378			formatEventDays(start, end) {
379				const startDay = start.getDate();
380				const endDay = end.getDate();
381				const sameMonth =
382					start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear();
383
384				if (sameMonth && startDay !== endDay) {
385					return `${startDay}-${endDay}`;
386				}
387
388				if (startDay === endDay && sameMonth) {
389					return String(startDay);
390				}
391
392				const startLabel = start.toLocaleString('en-US', { month: 'short', day: 'numeric' });
393				const endLabel = end.toLocaleString('en-US', { month: 'short', day: 'numeric' });
394				return `${startLabel}-${endLabel}`;
395			},
396
397			getEventBadge(start) {
398				const now = new Date();
399				now.setHours(0, 0, 0, 0);
400
401				const monday = new Date(now);
402				const day = now.getDay();
403				monday.setDate(now.getDate() - ((day + 6) % 7));
404
405				const friday = new Date(monday);
406				friday.setDate(monday.getDate() + 4);
407				friday.setHours(0, 0, 0, 0);
408
409				const sunday = new Date(friday);
410				sunday.setDate(friday.getDate() + 2);
411				sunday.setHours(23, 59, 59, 999);
412
413				const startDay = new Date(start);
414				startDay.setHours(0, 0, 0, 0);
415
416				if (startDay >= friday && startDay <= sunday) {
417					return 'This weekend';
418				}
419
420				return null;
421			},
422
423			async fetchEvents() {
424				if (!this.apiUrl) {
425					this.error = 'Events API is not configured.';
426					return;
427				}
428
429				this.loading = true;
430				this.error = null;
431
432				try {
433					const response = await fetch(this.apiUrl);
434
435					if (!response.ok) {
436						throw new Error('Failed to load events');
437					}
438
439					const data = await response.json();
440					this.events = Array.isArray(data) ? data.map((event) => this.mapApiEvent(event)) : [];
441				} catch (err) {
442					this.error = err?.message || 'Unable to load events. Please try again.';
443					this.events = [];
444				} finally {
445					this.loading = false;
446				}
447			},
448
449			async copyToClipboard(text = null) {
450				if (!text) {
451					return;
452				}
453
454				var success = false;
455
456				try {
457					await navigator.clipboard.writeText(text);
458					success = true;
459				} catch {
460					// Fallback for browsers without clipboard API
461					const input = document.createElement('textarea');
462					input.value = text;
463					input.setAttribute('readonly', '');
464					input.style.position = 'absolute';
465					input.style.left = '-9999px';
466					document.body.appendChild(input);
467					input.select();
468					document.execCommand('copy');
469					document.body.removeChild(input);
470				}
471				if (success) {
472					const el = this.$el;
473					const svgNs = 'http://www.w3.org/2000/svg';
474					const iconEl = document.createElementNS(svgNs, 'svg');
475
476					iconEl.setAttribute('class', 'w-4 h-4 text-green-500');
477					iconEl.setAttribute('fill', 'currentColor');
478					iconEl.setAttribute('viewBox', '0 0 20 20');
479
480					const path = document.createElementNS(svgNs, 'path');
481
482					path.setAttribute('fill-rule', 'evenodd');
483					path.setAttribute(
484						'd',
485						'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',
486					);
487					path.setAttribute('clip-rule', 'evenodd');
488					iconEl.appendChild(path);
489
490					el.classList.add('hidden');
491					el.insertAdjacentElement('afterend', iconEl);
492
493					setTimeout(() => {
494						iconEl.remove();
495						el.classList.remove('hidden');
496					}, 2000);
497				}
498			},
499		};
500	};
501
502	// Example: Add more components here
503	// window.alpineFunctions.anotherComponent = function () {
504	//     return { /* ... */ };
505	// };
506});
507;
508// Global Alpine.js functions
509(function() {
510	const registerGlobalFunctions = () => {
511	window.alpineFunctions = window.alpineFunctions || {};
512	window.alpineFunctions.backgroundImageComponent = function (srcset = '', fallback = '') {
513		return {
514			srcset,
515			fallback,
516			currentSrc: fallback,
517			_debounceTimeout: null,
518
519			init() {
520				this.update();
521				window.addEventListener('resize', this.debouncedUpdate.bind(this));
522			},
523			update() {
524				if (!this.srcset) {
525					return;
526				}
527				const sources = Array.from(this.srcset.matchAll(/(https?:\/\/.+?)(?=\s+\d+w)/g)).map((url, i, arr) => {
528					const widths = [...this.srcset.matchAll(/(\d+)w/g)];
529					return {
530						url: url[1],
531						width: parseInt(widths[i][1]),
532					};
533				}).filter(source => source.width !== source.height);
534				sources.sort((a, b) => a.width - b.width);
535				const viewport = window.innerWidth;			
536				let selected = sources[sources.length - 1]?.url || this.fallback;			
537				for (const source of sources) {
538					if (viewport <= source.width) {
539						selected = source.url;
540						break;
541					}
542				}
543				this.currentSrc = selected;
544			},
545			debouncedUpdate() {
546				clearTimeout(this._debounceTimeout);
547				this._debounceTimeout = setTimeout(() => {
548					this.update();
549				}, 250);
550			}
551		};
552	};
553
554	/**
555	 * Color Utils
556	 * Global helper functions for working with colors
557	 * - createGradientClass: Creates a gradient class for an element
558	 * - rotateHueAndLighten: Rotates a hue and lightens a color
559	 * - darken: Darkens a color
560	 * 
561	 * Gradient usage:
562	 * <div x-data x-init="
563	 *     $el.style.setProperty('--primary', $colorUtils.getVar('--site-primary'));
564	 *     $colorUtils.createGradientClass($el, 350, 44);
565	 * ">
566	 * $el is the element that the gradient class is being applied to
567	 * --primary is the variable that is being set to the site primary color
568	 * 350 is the hue offset
569	 * 44 is the lighten amount
570	 * 
571	 */
572	Alpine.magic('colorUtils', () => ({
573		getVar(name) {
574			return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
575		},
576		/**
577		 * hexToHsl: Converts a hex color to an hsl color
578		 */
579		hexToHsl(hex) {
580			hex = hex.replace('#', '');
581			let r = parseInt(hex.slice(0, 2), 16) / 255;
582			let g = parseInt(hex.slice(2, 4), 16) / 255;
583			let b = parseInt(hex.slice(4, 6), 16) / 255;
584
585			let max = Math.max(r, g, b), min = Math.min(r, g, b);
586			let h, s, l = (max + min) / 2;
587
588			if (max === min) {
589				h = s = 0;
590			} else {
591				let d = max - min;
592				s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
593				switch (max) {
594					case r: h = (g - b) / d + (g < b ? 6 : 0); break;
595					case g: h = (b - r) / d + 2; break;
596					case b: h = (r - g) / d + 4; break;
597				}
598				h /= 6;
599			}
600
601			return {
602				h: Math.round(h * 360),
603				s: Math.round(s * 100),
604				l: Math.round(l * 100),
605			};
606		},
607
608		/**
609		 * hslToHex: Converts an hsl color to a hex color
610		 */
611		hslToHex(h, s, l) {
612			s /= 100;
613			l /= 100;
614
615			const c = (1 - Math.abs(2 * l - 1)) * s;
616			const x = c * (1 - Math.abs((h / 60) % 2 - 1));
617			const m = l - c / 2;
618
619			let r, g, b;
620			if (h < 60) [r, g, b] = [c, x, 0];
621			else if (h < 120) [r, g, b] = [x, c, 0];
622			else if (h < 180) [r, g, b] = [0, c, x];
623			else if (h < 240) [r, g, b] = [0, x, c];
624			else if (h < 300) [r, g, b] = [x, 0, c];
625			else [r, g, b] = [c, 0, x];
626
627			r = Math.round((r + m) * 255);
628			g = Math.round((g + m) * 255);
629			b = Math.round((b + m) * 255);
630
631			return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`;
632		},
633		/**
634		 * rotateHueAndLighten: Rotates a hue and lightens a color
635		 * this is based on wheel of colors so we match the right contrast for the color
636		 * https://www.rapidtables.com/web/color/RGB_Color.html
637		 * 0 is the starting point of the wheel of colors based on the color passed in
638		 * 360 is the ending point of the wheel of colors
639		 */
640		rotateHueAndLighten(hex, hueOffset = 30, lightenAmount = 30) {
641			const hsl = this.hexToHsl(hex);
642			const newHue = (hsl.h + hueOffset) % 360;
643			/**Exeptions prep dig / redzone **/
644			/** those colors were too dark so we lighten them up a bit manually */
645			const exceptions = {213: 57, 357: 54};
646			if(Object.keys(exceptions).includes(hsl.h.toString())) {
647				lightenAmount = exceptions[hsl.h.toString()];
648			} 
649			const newLightness = Math.min(100, hsl.l + lightenAmount);
650			return this.hslToHex(newHue, hsl.s, newLightness);
651		},
652		/**
653		 * darken: Darkens a color
654		 */
655		darken(hex, percent) {
656			hex = hex.replace('#', '');
657			let r = parseInt(hex.substring(0, 2), 16);
658			let g = parseInt(hex.substring(2, 4), 16);
659			let b = parseInt(hex.substring(4, 6), 16);
660		
661			r = Math.max(0, Math.floor(r * (1 - percent / 100)));
662			g = Math.max(0, Math.floor(g * (1 - percent / 100)));
663			b = Math.max(0, Math.floor(b * (1 - percent / 100)));
664		
665			return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`;
666		},
667		/**
668		 * hexToRgba: Converts a hex color to rgba
669		 */
670		hexToRgba(hex, alpha) {
671			hex = hex.replace('#', '');
672			if (hex.length === 3) {
673				hex = hex.split('').map((char) => char + char).join('');
674			}
675
676			const r = parseInt(hex.slice(0, 2), 16);
677			const g = parseInt(hex.slice(2, 4), 16);
678			const b = parseInt(hex.slice(4, 6), 16);
679
680			return `rgba(${r}, ${g}, ${b}, ${alpha})`;
681		},
682		/**
683		 * createGradientClass: Creates a gradient class for an element
684		 *
685		 * @param {HTMLElement} el
686		 * @param {number} hueOffset
687		 * @param {number} lightenAmount
688		 * @param {boolean} border
689		 * @param {string} direction CSS linear-gradient direction (e.g. 'to right', 'to top')
690		 * @param {number|null} opacity Optional alpha for a lighter transparent wash (0–1)
691		 */
692		createGradientClass(el, hueOffset = 30, lightenAmount = 30, border = true, direction = 'to right', opacity = null) {
693			const primary = this.getVar('--site-primary');
694			const gradientDark = this.rotateHueAndLighten(primary, hueOffset+30, lightenAmount);
695			const gradientLight = this.rotateHueAndLighten(primary, hueOffset, lightenAmount-5);
696			const borderColor = this.rotateHueAndLighten(primary, 0, lightenAmount-3);
697
698			el.classList.add('dynamic-gradient');
699
700			if (opacity !== null) {
701				el.style.backgroundImage = `linear-gradient(${direction}, ${this.hexToRgba(gradientDark, opacity)}, ${this.hexToRgba(gradientLight, opacity * 0.25)})`;
702				if (border) {
703					el.style.borderColor = this.hexToRgba(borderColor, Math.min(opacity + 0.15, 0.45));
704				}
705			} else {
706				el.style.backgroundImage = `linear-gradient(${direction}, ${gradientDark}, ${gradientLight})`;
707				if (border) {
708					el.style.borderColor = borderColor;
709				}
710			}
711		},
712		clearGradientClass(el) {
713			el.classList.remove('dynamic-gradient');
714			el.style.backgroundImage = '';
715			el.style.borderColor = '';
716		}
717	}));
718
719	/**
720	 * Autocomplete Component - Initialization wrapper
721	 * Reads searchTypes from data attribute to avoid JSON encoding issues
722	 */
723	Alpine.data('autocompleteDataInit', (elementId, selectType, fieldToApply, inputId) => {
724		// Create a completely fresh instance each time
725		// We'll read searchTypes in init(), so start with empty array
726		return autocompleteDataFunction([], selectType, fieldToApply, elementId, inputId);
727	});
728	
729	/**
730	 * Autocomplete Component - Core function
731	 * Global Alpine.js data function for autocomplete search inputs
732	 */
733	function autocompleteDataFunction(searchTypes, selectType, fieldToApply, elementId, inputId) {
734		// Ensure searchTypes is a new array to avoid reference sharing
735		const searchTypesArray = Array.isArray(searchTypes) ? [...searchTypes] : [];
736		
737		return {
738			searchText: '',
739			searchTypes: searchTypesArray,
740			get isPlayerSearch() { 
741				return Array.isArray(this.searchTypes) && this.searchTypes.includes('players'); 
742			},
743			errorMessage: '',
744			isLoading: false,
745			showResults: false,
746			resultsHtml: '',
747			selectedItemId: null,
748			timer: null,
749			clickHandlerAdded: false, // Track if click handler has been added
750			get url() {
751				return (typeof ajax_object !== 'undefined' && ajax_object?.ajax_url) 
752					? ajax_object.ajax_url 
753					: '/wp-admin/admin-ajax.php';
754			},
755			fieldToApply: fieldToApply || '',
756			selectType: selectType || 'id',
757			elementId: elementId || '',
758			inputId: inputId || '',
759			
760			init() {
761				// Always read searchTypes from data attribute (it's the source of truth)
762				const element = document.getElementById(this.elementId);
763				if (element) {
764					try {
765						const dataAttr = element.getAttribute('data-search-types');
766						if (dataAttr) {
767							const parsed = JSON.parse(dataAttr);
768							if (Array.isArray(parsed)) {
769								this.searchTypes = parsed;
770							}
771						}
772					} catch (e) {
773						console.error('Error parsing searchTypes from data attribute:', e);
774					}
775				}
776				
777				// Set initial value from input if it exists (but don't use value attribute to avoid conflicts with x-model)
778				this.$nextTick(() => {
779					const initialInput = document.getElementById(this.inputId);
780					if (initialInput && initialInput.hasAttribute('value') && initialInput.getAttribute('value')) {
781						this.searchText = initialInput.getAttribute('value');
782					}
783				});
784				
785				if (this.selectType === 'id') {
786					// Use event delegation on the results list to handle clicks
787					this.$watch('resultsHtml', () => {
788						this.$nextTick(() => {
789							setTimeout(() => {
790								const resultsList = document.querySelector(`#${this.elementId} ul#options`);
791								if (resultsList) {
792									// Initialize Alpine.js on the results list
793									if (typeof window.Alpine !== 'undefined' && window.Alpine.initTree) {
794										window.Alpine.initTree(resultsList);
795									}
796									
797									// Add click handler using event delegation (only once per component instance)
798									if (!this.clickHandlerAdded) {
799										resultsList.addEventListener('click', (e) => {
800											const li = e.target.closest('li');
801											if (li) {
802												e.preventDefault();
803												e.stopPropagation();
804												this.handleItemClick(li);
805											}
806										});
807										this.clickHandlerAdded = true;
808									}
809								}
810							}, 50);
811						});
812					});
813				}
814			},
815			
816			handleInput() {
817				if (this.isPlayerSearch || this.searchText.length === 0) {
818					this.errorMessage = '';
819				}
820				this.performSearch();
821			},
822			
823			handleFocus() {
824				if (this.isPlayerSearch) {
825					this.errorMessage = '';
826				}
827				if (this.resultsHtml && this.resultsHtml.trim() !== '') {
828					this.showResults = true;
829				}
830			},
831			
832			handleKeyup() {
833				if (this.isPlayerSearch) {
834					this.errorMessage = '';
835				}
836			},
837			
838			handlePaste() {
839				if (this.isPlayerSearch) {
840					this.errorMessage = '';
841				}
842			},
843			
844			handleBlur() {
845				if (this.isPlayerSearch && this.searchText.length === 0) {
846					this.errorMessage = '';
847				}
848				// Hide results when input loses focus (with a small delay to allow clicks on results)
849				setTimeout(() => {
850					// Only hide if focus hasn't moved to the results list
851					const resultsList = document.querySelector(`#${this.elementId} ul#options`);
852					const activeElement = document.activeElement;
853					if (resultsList && !resultsList.contains(activeElement)) {
854						this.showResults = false;
855					}
856				}, 200);
857			},
858			
859			handleClick() {
860				if (this.resultsHtml && this.resultsHtml.trim() !== '') {
861					this.showResults = true;
862				}
863			},
864			
865			performSearch() {
866				if (this.isPlayerSearch) {
867					this.errorMessage = '';
868				}
869				
870				const searchLength = this.searchText.length;
871				
872				// For player searches, require at least 2 words (first and last name)
873				if (this.isPlayerSearch) {
874					const words = this.searchText.trim().split(/\s+/).filter(word => word.length > 0);
875					if (words.length < 2 && this.searchText.trim().length > 0) {
876						// Show error message if user has typed something but not enough words
877						this.errorMessage = 'Please enter both first and last name to search for players.';
878						this.showResults = false;
879						this.resultsHtml = '';
880						this.isLoading = false;
881						if (this.fieldToApply && this.$refs.fieldToApply) {
882							this.$refs.fieldToApply.value = '';
883						}
884						return;
885					} else if (words.length < 2) {
886						// Field is empty or only whitespace - clear error and don't search
887						this.showResults = false;
888						this.resultsHtml = '';
889						this.errorMessage = '';
890						this.isLoading = false;
891						if (this.fieldToApply && this.$refs.fieldToApply) {
892							this.$refs.fieldToApply.value = '';
893						}
894						return;
895					}
896				}
897				
898				// For non-player searches, require at least 3 characters
899				if (!this.isPlayerSearch && searchLength < 3) {
900					this.errorMessage = 'Please enter at least three letters when searching';
901					this.showResults = false;
902					if (this.fieldToApply && this.$refs.fieldToApply) {
903						this.$refs.fieldToApply.value = '';
904					}
905					return;
906				}
907				
908				this.errorMessage = '';
909				this.isLoading = true;
910				this.resultsHtml = '';
911				
912				// Ensure URL is set correctly
913				const ajaxUrl = (typeof ajax_object !== 'undefined' && ajax_object?.ajax_url) 
914					? ajax_object.ajax_url 
915					: '/wp-admin/admin-ajax.php';
916				
917				fetch(ajaxUrl, {
918					method: 'POST',
919					headers: {
920						'Content-Type': 'application/x-www-form-urlencoded',
921					},
922					credentials: 'same-origin',
923					body: new URLSearchParams({
924						itemName: this.searchText,
925						postTypes: JSON.stringify(this.searchTypes),
926						selectType: this.selectType,
927						action: 'get_items_for_autocomplete'
928					})
929				})
930				.then(response => {
931					if (!response.ok) {
932						throw new Error(`HTTP error! status: ${response.status}`);
933					}
934					return response.text();
935				})
936				.then(html => {
937					this.isLoading = false;
938					const hasResults = html.trim() !== '';
939					
940					if (hasResults) {
941						// Set HTML first
942						this.resultsHtml = html;
943						
944						// Wait for DOM to update, then show and initialize Alpine
945						this.$nextTick(() => {
946							// Set showResults after DOM is updated
947							this.showResults = true;
948							
949							// Explicitly remove hidden class if it exists
950							setTimeout(() => {
951								const resultsList = document.querySelector(`#${this.elementId} ul#options`);
952								if (resultsList) {
953									resultsList.classList.remove('hidden');
954									
955									// Initialize Alpine.js on the inserted HTML
956									if (typeof window.Alpine !== 'undefined' && window.Alpine.initTree) {
957										try {
958											window.Alpine.initTree(resultsList);
959										} catch (e) {
960											console.error('Error initializing Alpine on results:', e);
961										}
962									}
963								}
964							}, 50);
965						});
966					} else {
967						this.resultsHtml = '';
968						this.showResults = false;
969					}
970				})
971				.catch(error => {
972					this.isLoading = false;
973					this.errorMessage = 'An error occurred while searching. Please try again.';
974					this.showResults = false;
975					console.error('Autocomplete error:', error);
976					console.error('Search text:', this.searchText);
977					console.error('Search types:', this.searchTypes);
978					console.error('AJAX URL:', ajaxUrl);
979				});
980			},
981			
982			handleItemClick(li) {
983				const isChecked = li.classList.contains('checked');
984				const allItems = document.querySelectorAll(`#${this.elementId} li`);
985				
986				if (isChecked) {
987					li.classList.remove('checked');
988					const checkIcon = li.querySelector('.item-check');
989					if (checkIcon) checkIcon.classList.add('hidden');
990					this.searchText = '';
991					if (this.fieldToApply && this.$refs.fieldToApply) {
992						this.$refs.fieldToApply.value = '';
993					}
994				} else {
995					allItems.forEach(item => {
996						item.classList.remove('checked');
997						const checkIcon = item.querySelector('.item-check');
998						if (checkIcon) checkIcon.classList.add('hidden');
999					});
1000					
1001					li.classList.add('checked');
1002					const checkIcon = li.querySelector('.item-check');
1003					if (checkIcon) checkIcon.classList.remove('hidden');
1004					
1005					const itemName = li.querySelector('.item-name');
1006					if (itemName) {
1007						this.searchText = itemName.textContent.trim();
1008					}
1009					
1010					this.showResults = false;
1011					
1012					if (this.fieldToApply && this.$refs.fieldToApply) {
1013						const itemWrapper = li.querySelector('.item-wrapper');
1014						if (itemWrapper && itemWrapper.dataset && itemWrapper.dataset.id) {
1015							this.$refs.fieldToApply.value = itemWrapper.dataset.id;
1016						} else if (itemWrapper && itemWrapper.getAttribute('data-id')) {
1017							this.$refs.fieldToApply.value = itemWrapper.getAttribute('data-id');
1018						}
1019					}
1020				}
1021			}
1022		};
1023	}
1024	
1025		// Also register as a direct function for backwards compatibility
1026		Alpine.data('autocompleteData', (searchTypes, selectType, fieldToApply, elementId, inputId) => {
1027			return autocompleteDataFunction(searchTypes, selectType, fieldToApply, elementId, inputId);
1028		});
1029	};
1030	
1031	// Register immediately if Alpine is available, otherwise wait for alpine:init
1032	if (typeof window.Alpine !== 'undefined') {
1033		registerGlobalFunctions();
1034	} else {
1035		document.addEventListener('alpine:init', registerGlobalFunctions);
1036	}
1037})();
1038
1039;

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.