// In alpineFunctions.js /** * Alpine Functions * This file contains the Alpine functions for the Prep Network Content theme */ document.addEventListener('alpine:init', () => { window.alpineFunctions = window.alpineFunctions || {}; /** * Background Image Component * This component is used to load the background image * It uses the srcset to load the image and the fallback to load the image if the srcset is not available * It also uses the debouncedUpdate to update the image when the window is resized */ window.alpineFunctions.backgroundImageComponent = function (srcset = '', fallback = '') { return { srcset, fallback, currentSrc: fallback, _debounceTimeout: null, init() { this.update(); window.addEventListener('resize', this.debouncedUpdate.bind(this)); }, update() { if (!this.srcset) { return; } const sources = Array.from(this.srcset.matchAll(/(https?:\/\/.+?)(?=\s+\d+w)/g)).map((url, i, arr) => { const widths = [...this.srcset.matchAll(/(\d+)w/g)]; return { url: url[1], width: parseInt(widths[i][1]), }; }).filter(source => source.width !== source.height); sources.sort((a, b) => a.width - b.width); const viewport = window.innerWidth; let selected = sources[sources.length - 1]?.url || this.fallback; for (const source of sources) { if (viewport <= source.width) { selected = source.url; break; } } this.currentSrc = selected; }, debouncedUpdate() { clearTimeout(this._debounceTimeout); this._debounceTimeout = setTimeout(() => { this.update(); }, 250); } }; }; /** * Lazy Background Component * This component is used to lazy load the background image * It uses the Intersection Observer API to load the image when it is in view * It also uses the backgroundImageComponent to handle the image loading and resizing */ Alpine.data('lazyBackgroundComponent', (srcset, fallback) => ({ observer: null, loaded: false, child: null, // this will hold the backgroundImageComponent instance resizeHandler: null, // store handler reference for cleanup syncTimeout: null, // store timeout reference for cleanup isSliderSlide: false, // track if this is a slider slide currentSrc: '', // exposed for Blade x-bind init() { // Check if element has data-slide-index attribute (slider slides) this.isSliderSlide = this.$el.hasAttribute('data-slide-index'); if (this.isSliderSlide) { // For slider slides, set currentSrc to empty initially // They will be loaded manually via Slick events this.currentSrc = ''; // Don't create IntersectionObserver - will be loaded manually return; } // For non-slider elements, use fallback and IntersectionObserver this.currentSrc = fallback; this.observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { this.loadBackground(); this.observer.disconnect(); } }, { rootMargin: '200px', // preload slightly before visibility }); this.observer.observe(this.$el); }, loadBackground() { if (this.loaded) return; this.loaded = true; // initialize the responsive component dynamically this.child = window.alpineFunctions.backgroundImageComponent(srcset, fallback); // Manually run its init + update routine this.child.init(); this.child.update(); // Sync the background to the wrapper value this.currentSrc = this.child.currentSrc; // Preload the image to improve perceived load time if (this.currentSrc) { const img = new Image(); img.src = this.currentSrc; } // Keep them in sync on resize // Store handler reference for cleanup this.resizeHandler = () => { if (!this.child) return; // safety check this.child.debouncedUpdate(); // Clear any pending sync timeout if (this.syncTimeout) { clearTimeout(this.syncTimeout); } // Sync after debounce completes (250ms delay) this.syncTimeout = setTimeout(() => { if (this.child) { this.currentSrc = this.child.currentSrc; } this.syncTimeout = null; }, 250); }; window.addEventListener('resize', this.resizeHandler); }, destroy() { // Cleanup: remove resize listener, clear timeouts, and disconnect observer if (this.resizeHandler) { window.removeEventListener('resize', this.resizeHandler); this.resizeHandler = null; } if (this.syncTimeout) { clearTimeout(this.syncTimeout); this.syncTimeout = null; } if (this.observer) { this.observer.disconnect(); this.observer = null; } } })); /** * Color Utils * Global helper functions for working with colors * - createGradientClass: Creates a gradient class for an element * - rotateHueAndLighten: Rotates a hue and lightens a color * - darken: Darkens a color * * Gradient usage: *
* $el is the element that the gradient class is being applied to * --primary is the variable that is being set to the site primary color * 350 is the hue offset * 44 is the lighten amount * */ Alpine.magic('colorUtils', () => ({ getVar(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }, /** * hexToHsl: Converts a hex color to an hsl color */ hexToHsl(hex) { hex = hex.replace('#', ''); let r = parseInt(hex.slice(0, 2), 16) / 255; let g = parseInt(hex.slice(2, 4), 16) / 255; let b = parseInt(hex.slice(4, 6), 16) / 255; let max = Math.max(r, g, b), min = Math.min(r, g, b); let h, s, l = (max + min) / 2; if (max === min) { h = s = 0; } else { let d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100), }; }, /** * hslToHex: Converts an hsl color to a hex color */ hslToHex(h, s, l) { s /= 100; l /= 100; const c = (1 - Math.abs(2 * l - 1)) * s; const x = c * (1 - Math.abs((h / 60) % 2 - 1)); const m = l - c / 2; let r, g, b; if (h < 60) [r, g, b] = [c, x, 0]; else if (h < 120) [r, g, b] = [x, c, 0]; else if (h < 180) [r, g, b] = [0, c, x]; else if (h < 240) [r, g, b] = [0, x, c]; else if (h < 300) [r, g, b] = [x, 0, c]; else [r, g, b] = [c, 0, x]; r = Math.round((r + m) * 255); g = Math.round((g + m) * 255); b = Math.round((b + m) * 255); return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`; }, /** * rotateHueAndLighten: Rotates a hue and lightens a color * this is based on wheel of colors so we match the right contrast for the color * https://www.rapidtables.com/web/color/RGB_Color.html * 0 is the starting point of the wheel of colors based on the color passed in * 360 is the ending point of the wheel of colors */ rotateHueAndLighten(hex, hueOffset = 30, lightenAmount = 30) { const hsl = this.hexToHsl(hex); const newHue = (hsl.h + hueOffset) % 360; /**Exeptions prep dig / redzone **/ /** those colors were too dark so we lighten them up a bit manually */ const exceptions = {213: 57, 357: 54}; if(Object.keys(exceptions).includes(hsl.h.toString())) { lightenAmount = exceptions[hsl.h.toString()]; } const newLightness = Math.min(100, hsl.l + lightenAmount); return this.hslToHex(newHue, hsl.s, newLightness); }, /** * darken: Darkens a color */ darken(hex, percent) { hex = hex.replace('#', ''); let r = parseInt(hex.substring(0, 2), 16); let g = parseInt(hex.substring(2, 4), 16); let b = parseInt(hex.substring(4, 6), 16); r = Math.max(0, Math.floor(r * (1 - percent / 100))); g = Math.max(0, Math.floor(g * (1 - percent / 100))); b = Math.max(0, Math.floor(b * (1 - percent / 100))); return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`; }, /** * createGradientClass: Creates a gradient class for an element */ createGradientClass(el, hueOffset = 30, lightenAmount = 30, border = true) { const primary = this.getVar('--site-primary'); const gradientDark = this.rotateHueAndLighten(primary, hueOffset+30, lightenAmount); const gradientLight = this.rotateHueAndLighten(primary, hueOffset, lightenAmount-5); el.classList.add('dynamic-gradient'); el.style.backgroundImage = `linear-gradient(to right, ${gradientDark}, ${gradientLight})`; el.style.borderColor = this.rotateHueAndLighten(primary, 0, lightenAmount-3); if (border) { el.style.borderColor = this.rotateHueAndLighten(primary, 0, lightenAmount-3); } } })); window.alpineFunctions.scrollOnClick = function () { return { scrollToId(id, offset = 0) { const el = document.getElementById(id); if (!el) return; const y = el.getBoundingClientRect().top + window.scrollY - offset; window.scrollTo({ top: y, behavior: 'smooth' }); }, init() { const raw = window.location.search; const safeSearch = encodeURI(raw); const params = new URLSearchParams(safeSearch); if (params.has('prompt_claim')) { setTimeout(() => { this.scrollToId('verify-player', 300); }, 500); } } } }; // Example: Add more components here // window.alpineFunctions.anotherComponent = function () { // return { /* ... */ }; // }; });; /** * Hero Section Video Modal Component */ (function() { const registerComponent = () => { window.alpineFunctions = window.alpineFunctions || {}; window.alpineFunctions.heroVideoModal = function () { return { showVideo: false, modalId: 'hero-video-modal-' + Math.random().toString(36).slice(2, 11), openModal() { this.showVideo = true; document.body.classList.add('overflow-hidden'); // Play video after modal opens this.$nextTick(() => { const modal = document.getElementById(this.modalId); if (!modal) return; const video = modal.querySelector('video'); const iframe = modal.querySelector('iframe'); if (video) { // Load and play the video video.load(); // Give the video a moment to start loading setTimeout(() => { video.currentTime = 0; video.play().catch(() => { // If autoplay fails, controls are visible for manual play }); }, 300); } else if (iframe && iframe.src) { // For YouTube, ensure autoplay with sound if (iframe.src.includes('youtube.com')) { let newSrc = iframe.src; if (!newSrc.includes('autoplay=')) { newSrc += (newSrc.includes('?') ? '&' : '?') + 'autoplay=1'; } if (newSrc.includes('mute=1')) { newSrc = newSrc.replace('mute=1', 'mute=0'); } else if (!newSrc.includes('mute=')) { newSrc += '&mute=0'; } iframe.src = newSrc; } } }); }, closeModal() { const modal = document.getElementById(this.modalId); if (modal) { // Stop all videos modal.querySelectorAll('video').forEach(v => { v.pause(); v.currentTime = 0; }); // Reset iframes modal.querySelectorAll('iframe').forEach(iframe => { const originalSrc = iframe.src; iframe.src = 'about:blank'; setTimeout(() => { iframe.src = originalSrc.replace(/[?&](autoplay|mute)=[^&]*/g, ''); }, 300); }); } this.showVideo = false; document.body.classList.remove('overflow-hidden'); }, init() { // Listen for ESC key this.$watch('showVideo', value => { if (value) { const escHandler = (e) => { if (e.key === 'Escape') { this.closeModal(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); } }); } }; }; }; // Register on alpine:init to ensure Alpine is loaded document.addEventListener('alpine:init', registerComponent); })();; // Global Alpine.js functions (function() { const registerGlobalFunctions = () => { window.alpineFunctions = window.alpineFunctions || {}; window.alpineFunctions.backgroundImageComponent = function (srcset = '', fallback = '') { return { srcset, fallback, currentSrc: fallback, _debounceTimeout: null, init() { this.update(); window.addEventListener('resize', this.debouncedUpdate.bind(this)); }, update() { if (!this.srcset) { return; } const sources = Array.from(this.srcset.matchAll(/(https?:\/\/.+?)(?=\s+\d+w)/g)).map((url, i, arr) => { const widths = [...this.srcset.matchAll(/(\d+)w/g)]; return { url: url[1], width: parseInt(widths[i][1]), }; }).filter(source => source.width !== source.height); sources.sort((a, b) => a.width - b.width); const viewport = window.innerWidth; let selected = sources[sources.length - 1]?.url || this.fallback; for (const source of sources) { if (viewport <= source.width) { selected = source.url; break; } } this.currentSrc = selected; }, debouncedUpdate() { clearTimeout(this._debounceTimeout); this._debounceTimeout = setTimeout(() => { this.update(); }, 250); } }; }; /** * Color Utils * Global helper functions for working with colors * - createGradientClass: Creates a gradient class for an element * - rotateHueAndLighten: Rotates a hue and lightens a color * - darken: Darkens a color * * Gradient usage: *
* $el is the element that the gradient class is being applied to * --primary is the variable that is being set to the site primary color * 350 is the hue offset * 44 is the lighten amount * */ Alpine.magic('colorUtils', () => ({ getVar(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }, /** * hexToHsl: Converts a hex color to an hsl color */ hexToHsl(hex) { hex = hex.replace('#', ''); let r = parseInt(hex.slice(0, 2), 16) / 255; let g = parseInt(hex.slice(2, 4), 16) / 255; let b = parseInt(hex.slice(4, 6), 16) / 255; let max = Math.max(r, g, b), min = Math.min(r, g, b); let h, s, l = (max + min) / 2; if (max === min) { h = s = 0; } else { let d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100), }; }, /** * hslToHex: Converts an hsl color to a hex color */ hslToHex(h, s, l) { s /= 100; l /= 100; const c = (1 - Math.abs(2 * l - 1)) * s; const x = c * (1 - Math.abs((h / 60) % 2 - 1)); const m = l - c / 2; let r, g, b; if (h < 60) [r, g, b] = [c, x, 0]; else if (h < 120) [r, g, b] = [x, c, 0]; else if (h < 180) [r, g, b] = [0, c, x]; else if (h < 240) [r, g, b] = [0, x, c]; else if (h < 300) [r, g, b] = [x, 0, c]; else [r, g, b] = [c, 0, x]; r = Math.round((r + m) * 255); g = Math.round((g + m) * 255); b = Math.round((b + m) * 255); return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`; }, /** * rotateHueAndLighten: Rotates a hue and lightens a color * this is based on wheel of colors so we match the right contrast for the color * https://www.rapidtables.com/web/color/RGB_Color.html * 0 is the starting point of the wheel of colors based on the color passed in * 360 is the ending point of the wheel of colors */ rotateHueAndLighten(hex, hueOffset = 30, lightenAmount = 30) { const hsl = this.hexToHsl(hex); const newHue = (hsl.h + hueOffset) % 360; /**Exeptions prep dig / redzone **/ /** those colors were too dark so we lighten them up a bit manually */ const exceptions = {213: 57, 357: 54}; if(Object.keys(exceptions).includes(hsl.h.toString())) { lightenAmount = exceptions[hsl.h.toString()]; } const newLightness = Math.min(100, hsl.l + lightenAmount); return this.hslToHex(newHue, hsl.s, newLightness); }, /** * darken: Darkens a color */ darken(hex, percent) { hex = hex.replace('#', ''); let r = parseInt(hex.substring(0, 2), 16); let g = parseInt(hex.substring(2, 4), 16); let b = parseInt(hex.substring(4, 6), 16); r = Math.max(0, Math.floor(r * (1 - percent / 100))); g = Math.max(0, Math.floor(g * (1 - percent / 100))); b = Math.max(0, Math.floor(b * (1 - percent / 100))); return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`; }, /** * createGradientClass: Creates a gradient class for an element */ createGradientClass(el, hueOffset = 30, lightenAmount = 30, border = true) { const primary = this.getVar('--site-primary'); const gradientDark = this.rotateHueAndLighten(primary, hueOffset+30, lightenAmount); const gradientLight = this.rotateHueAndLighten(primary, hueOffset, lightenAmount-5); el.classList.add('dynamic-gradient'); el.style.backgroundImage = `linear-gradient(to right, ${gradientDark}, ${gradientLight})`; el.style.borderColor = this.rotateHueAndLighten(primary, 0, lightenAmount-3); if (border) { el.style.borderColor = this.rotateHueAndLighten(primary, 0, lightenAmount-3); } } })); /** * Autocomplete Component - Initialization wrapper * Reads searchTypes from data attribute to avoid JSON encoding issues */ Alpine.data('autocompleteDataInit', (elementId, selectType, fieldToApply, inputId) => { // Create a completely fresh instance each time // We'll read searchTypes in init(), so start with empty array return autocompleteDataFunction([], selectType, fieldToApply, elementId, inputId); }); /** * Autocomplete Component - Core function * Global Alpine.js data function for autocomplete search inputs */ function autocompleteDataFunction(searchTypes, selectType, fieldToApply, elementId, inputId) { // Ensure searchTypes is a new array to avoid reference sharing const searchTypesArray = Array.isArray(searchTypes) ? [...searchTypes] : []; return { searchText: '', searchTypes: searchTypesArray, get isPlayerSearch() { return Array.isArray(this.searchTypes) && this.searchTypes.includes('players'); }, errorMessage: '', isLoading: false, showResults: false, resultsHtml: '', selectedItemId: null, timer: null, clickHandlerAdded: false, // Track if click handler has been added get url() { return (typeof ajax_object !== 'undefined' && ajax_object?.ajax_url) ? ajax_object.ajax_url : '/wp-admin/admin-ajax.php'; }, fieldToApply: fieldToApply || '', selectType: selectType || 'id', elementId: elementId || '', inputId: inputId || '', init() { // Always read searchTypes from data attribute (it's the source of truth) const element = document.getElementById(this.elementId); if (element) { try { const dataAttr = element.getAttribute('data-search-types'); if (dataAttr) { const parsed = JSON.parse(dataAttr); if (Array.isArray(parsed)) { this.searchTypes = parsed; } } } catch (e) { console.error('Error parsing searchTypes from data attribute:', e); } } // Set initial value from input if it exists (but don't use value attribute to avoid conflicts with x-model) this.$nextTick(() => { const initialInput = document.getElementById(this.inputId); if (initialInput && initialInput.hasAttribute('value') && initialInput.getAttribute('value')) { this.searchText = initialInput.getAttribute('value'); } }); if (this.selectType === 'id') { // Use event delegation on the results list to handle clicks this.$watch('resultsHtml', () => { this.$nextTick(() => { setTimeout(() => { const resultsList = document.querySelector(`#${this.elementId} ul#options`); if (resultsList) { // Initialize Alpine.js on the results list if (typeof window.Alpine !== 'undefined' && window.Alpine.initTree) { window.Alpine.initTree(resultsList); } // Add click handler using event delegation (only once per component instance) if (!this.clickHandlerAdded) { resultsList.addEventListener('click', (e) => { const li = e.target.closest('li'); if (li) { e.preventDefault(); e.stopPropagation(); this.handleItemClick(li); } }); this.clickHandlerAdded = true; } } }, 50); }); }); } }, handleInput() { if (this.isPlayerSearch || this.searchText.length === 0) { this.errorMessage = ''; } this.performSearch(); }, handleFocus() { if (this.isPlayerSearch) { this.errorMessage = ''; } if (this.resultsHtml && this.resultsHtml.trim() !== '') { this.showResults = true; } }, handleKeyup() { if (this.isPlayerSearch) { this.errorMessage = ''; } }, handlePaste() { if (this.isPlayerSearch) { this.errorMessage = ''; } }, handleBlur() { if (this.isPlayerSearch && this.searchText.length === 0) { this.errorMessage = ''; } // Hide results when input loses focus (with a small delay to allow clicks on results) setTimeout(() => { // Only hide if focus hasn't moved to the results list const resultsList = document.querySelector(`#${this.elementId} ul#options`); const activeElement = document.activeElement; if (resultsList && !resultsList.contains(activeElement)) { this.showResults = false; } }, 200); }, handleClick() { if (this.resultsHtml && this.resultsHtml.trim() !== '') { this.showResults = true; } }, performSearch() { if (this.isPlayerSearch) { this.errorMessage = ''; } const searchLength = this.searchText.length; // For player searches, require at least 2 words (first and last name) if (this.isPlayerSearch) { const words = this.searchText.trim().split(/\s+/).filter(word => word.length > 0); if (words.length < 2 && this.searchText.trim().length > 0) { // Show error message if user has typed something but not enough words this.errorMessage = 'Please enter both first and last name to search for players.'; this.showResults = false; this.resultsHtml = ''; this.isLoading = false; if (this.fieldToApply && this.$refs.fieldToApply) { this.$refs.fieldToApply.value = ''; } return; } else if (words.length < 2) { // Field is empty or only whitespace - clear error and don't search this.showResults = false; this.resultsHtml = ''; this.errorMessage = ''; this.isLoading = false; if (this.fieldToApply && this.$refs.fieldToApply) { this.$refs.fieldToApply.value = ''; } return; } } // For non-player searches, require at least 3 characters if (!this.isPlayerSearch && searchLength < 3) { this.errorMessage = 'Please enter at least three letters when searching'; this.showResults = false; if (this.fieldToApply && this.$refs.fieldToApply) { this.$refs.fieldToApply.value = ''; } return; } this.errorMessage = ''; this.isLoading = true; this.resultsHtml = ''; // Ensure URL is set correctly const ajaxUrl = (typeof ajax_object !== 'undefined' && ajax_object?.ajax_url) ? ajax_object.ajax_url : '/wp-admin/admin-ajax.php'; fetch(ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, credentials: 'same-origin', body: new URLSearchParams({ itemName: this.searchText, postTypes: JSON.stringify(this.searchTypes), selectType: this.selectType, action: 'get_items_for_autocomplete' }) }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.text(); }) .then(html => { this.isLoading = false; const hasResults = html.trim() !== ''; if (hasResults) { // Set HTML first this.resultsHtml = html; // Wait for DOM to update, then show and initialize Alpine this.$nextTick(() => { // Set showResults after DOM is updated this.showResults = true; // Explicitly remove hidden class if it exists setTimeout(() => { const resultsList = document.querySelector(`#${this.elementId} ul#options`); if (resultsList) { resultsList.classList.remove('hidden'); // Initialize Alpine.js on the inserted HTML if (typeof window.Alpine !== 'undefined' && window.Alpine.initTree) { try { window.Alpine.initTree(resultsList); } catch (e) { console.error('Error initializing Alpine on results:', e); } } } }, 50); }); } else { this.resultsHtml = ''; this.showResults = false; } }) .catch(error => { this.isLoading = false; this.errorMessage = 'An error occurred while searching. Please try again.'; this.showResults = false; console.error('Autocomplete error:', error); console.error('Search text:', this.searchText); console.error('Search types:', this.searchTypes); console.error('AJAX URL:', ajaxUrl); }); }, handleItemClick(li) { const isChecked = li.classList.contains('checked'); const allItems = document.querySelectorAll(`#${this.elementId} li`); if (isChecked) { li.classList.remove('checked'); const checkIcon = li.querySelector('.item-check'); if (checkIcon) checkIcon.classList.add('hidden'); this.searchText = ''; if (this.fieldToApply && this.$refs.fieldToApply) { this.$refs.fieldToApply.value = ''; } } else { allItems.forEach(item => { item.classList.remove('checked'); const checkIcon = item.querySelector('.item-check'); if (checkIcon) checkIcon.classList.add('hidden'); }); li.classList.add('checked'); const checkIcon = li.querySelector('.item-check'); if (checkIcon) checkIcon.classList.remove('hidden'); const itemName = li.querySelector('.item-name'); if (itemName) { this.searchText = itemName.textContent.trim(); } this.showResults = false; if (this.fieldToApply && this.$refs.fieldToApply) { const itemWrapper = li.querySelector('.item-wrapper'); if (itemWrapper && itemWrapper.dataset && itemWrapper.dataset.id) { this.$refs.fieldToApply.value = itemWrapper.dataset.id; } else if (itemWrapper && itemWrapper.getAttribute('data-id')) { this.$refs.fieldToApply.value = itemWrapper.getAttribute('data-id'); } } } } }; } // Also register as a direct function for backwards compatibility Alpine.data('autocompleteData', (searchTypes, selectType, fieldToApply, elementId, inputId) => { return autocompleteDataFunction(searchTypes, selectType, fieldToApply, elementId, inputId); }); }; // Register immediately if Alpine is available, otherwise wait for alpine:init if (typeof window.Alpine !== 'undefined') { registerGlobalFunctions(); } else { document.addEventListener('alpine:init', registerGlobalFunctions); } })(); ;