// 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 { /* ... */ };
// };
});;
// 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);
}
})();
;