(async () => {
if (!document.querySelector(".ex-rewards")) return;
document.querySelector(".new-rewards-row").classList.remove("hide");
const pageContent = document.querySelector(".page-content");
const oldRewards = pageContent.querySelector(":scope > .rewards");
oldRewards.classList.add("rewards-native", "hide");
const newRewards = document.querySelector(".new-rewards-section");
const newRewardItemsList = newRewards.querySelector(".new-reward-items-list");
const rewardItemsTitle = newRewards.querySelector(".reward-items-title span");
const filterSection = newRewards.querySelector(".rewards-filter");
const rewardItems = document.querySelectorAll("#rewardItemsList .reward-item");
const allItems = [];
const allItemsFiltering = [];
// Apply User name
newRewards.querySelector(".points-info__title span").textContent = EvoXLayer().user.name;
// ===== HELPERS ===== //
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const fetchWithRetry = async (path, retries = 3, delay = 1000) => {
for (let attempt = 0; attempt < retries; attempt++) {
const response = await fetch(`${window.location.origin}${path}`, {
method: "GET",
headers: { Accept: "text/html" },
credentials: "include",
});
if (response.status === 429) {
console.warn(`Rate limited on ${path}, retrying in ${delay * (attempt + 1)}ms…`);
await sleep(delay * (attempt + 1));
continue;
}
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response;
}
throw new Error(`Failed after ${retries} retries: ${path}`);
};
// ===== CLASSES ===== //
// ******************* //
class VirtualProductFilter {
constructor(allItems, config = {}) {
this.allItems = allItems;
this.title = config.title;
this.container = config.container;
this.itemsPerPage = config.itemsPerPage || 50;
this.buffer = config.buffer || 10;
this.title.textContent = `${this.allItems.length} ${this.allItems.length === 1 ? "reward" : "rewards"} found`;
this.products = this.extractProductData(allItems);
this.buildIndices();
this.filteredProducts = [...this.products];
this.renderedItems = new Set();
this.currentPage = 0;
this.setupVirtualContainer();
this.setupIntersectionObserver();
this.createLoadMoreButton();
}
createLoadMoreButton() {
this.loadMoreBtn = document.createElement("button");
this.loadMoreBtn.classList = "btn btn-primary btn-load-more";
this.loadMoreBtn.innerHTML = "Load More";
this.loadMoreBtn.style.cssText = "display: none; margin: 20px auto; padding: 12px 30px;";
this.loadMoreBtn.addEventListener("click", () => {
this.loadMoreItems();
});
this.container.appendChild(this.loadMoreBtn);
this.updateLoadMoreButton();
}
updateLoadMoreButton() {
const hasMore = this.renderedItems.size < this.filteredProducts.length;
if (hasMore) {
this.loadMoreBtn.style.display = "block";
const remaining = this.filteredProducts.length - this.renderedItems.size;
this.loadMoreBtn.innerHTML = `Load More (${remaining} remaining)`;
} else {
this.loadMoreBtn.style.display = "none";
}
}
extractProductData(elements) {
return elements.map((el, index) => ({
id: index,
element: el,
title: el.querySelector(".product-title")?.textContent.trim() || "",
sku: el.querySelector(".product-details-sku")?.textContent.replace("Product Code", "").trim(),
category: el.querySelector(".product-details-category a")?.textContent.trim() || "",
brand: el.querySelector(".product-details-brand img")?.alt || "",
price: Number((el.querySelector(".product-price")?.textContent || "").trim().replace(/,/g, "")) || 0,
disabled: el.querySelector("button")?.hasAttribute("disabled") || false,
}));
}
buildIndices() {
this.indices = {
byCategory: new Map(),
byPrice: [...this.products].sort((a, b) => a.price - b.price),
};
Object.keys(rewardsCategoryFilterOptions).forEach((category) => {
this.indices.byCategory.set(category, []);
});
this.indices.byCategory.set("Other", []);
this.products.forEach((product) => {
const categoryName = product.category || "";
const sku = product.sku || "";
let matchedGroup = "Other";
for (let [group, regex] of Object.entries(rewardsCategoryFilterOptions)) {
if (regex.test(categoryName) || regex.test(sku)) {
matchedGroup = group;
break;
}
}
this.indices.byCategory.get(matchedGroup).push(product);
});
}
setupVirtualContainer() {
this.container.replaceChildren();
this.spacer = document.createElement("div");
this.spacer.style.height = "0px";
this.container.append(this.spacer);
this.viewport = document.createElement("div");
this.viewport.classList = "row items-row";
this.container.append(this.viewport);
}
setupIntersectionObserver() {
const sentinel = document.createElement("div");
sentinel.style.height = "1px";
sentinel.dataset.sentinel = "true";
this.container.appendChild(sentinel);
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) this.loadMoreItems();
});
},
{ rootMargin: "200px" },
);
this.observer.observe(sentinel);
}
filter({ category, minPrice, maxPrice, search } = {}) {
let result = this.products;
if (category !== undefined && category !== null && category !== "all") {
result = this.indices.byCategory.get(category.toLowerCase()) || [];
}
if (minPrice !== undefined || maxPrice !== undefined) {
const min = minPrice || 0;
const max = maxPrice || Infinity;
result = result.filter((p) => p.price >= min && p.price <= max);
}
if (search) {
const term = search.toLowerCase();
result = result.filter((p) => p.title.toLowerCase().includes(term) || p.sku.toLowerCase().includes(term));
}
this.filteredProducts = result;
this.currentPage = 0;
this.renderedItems.clear();
this.viewport.replaceChildren();
this.title.textContent = `${this.filteredProducts.length} ${this.filteredProducts.length === 1 ? "reward" : "rewards"} found`;
this.renderBatch(0, this.itemsPerPage);
this.updateLoadMoreButton();
}
search(term) {
return this.filter({ search: term });
}
reset() {
this.filteredProducts = [...this.products];
this.currentPage = 0;
this.renderedItems.clear();
this.viewport.replaceChildren();
this.renderBatch(0, this.itemsPerPage);
this.updateLoadMoreButton();
return this.products.length;
}
renderBatch(start, count) {
if (!this.filteredProducts.length) {
this.viewport.replaceChildren();
const rewardItem = document.createElement("div");
rewardItem.classList = "col-sm-12 reward-item reward-item-placeholder";
rewardItem.innerHTML = `

No rewards found
Try adjusting your filters to see more options
`;
this.viewport.appendChild(rewardItem);
this.updateLoadMoreButton();
return;
}
this.viewport.querySelectorAll(".reward-item-placeholder").forEach((e) => e.remove());
const end = Math.min(start + count, this.filteredProducts.length);
const fragment = document.createDocumentFragment();
const clones = [];
for (let i = start; i < end; i++) {
if (this.renderedItems.has(i)) continue;
const product = this.filteredProducts[i];
const clone = product.element.cloneNode(true);
fragment.appendChild(clone);
clones.push(clone);
this.renderedItems.add(i);
}
this.viewport.appendChild(fragment);
clones.forEach((clone) => setTimeout(() => clone.classList.add("reward-item-new"), 10));
this.updateLoadMoreButton();
}
loadMoreItems() {
const nextStart = this.renderedItems.size;
if (nextStart < this.filteredProducts.length) {
this.renderBatch(nextStart, this.itemsPerPage);
this.updateLoadMoreButton();
}
}
}
// *********************** //
// ===== END CLASSES ===== //
// ===== FUNCTIONS ===== //
// ********************* //
const getOrCreateProductObject = (scriptElement) => {
const scriptContent = scriptElement.textContent;
const objNameMatch = scriptContent.match(/var\s+(obj_\d+)/);
if (!objNameMatch) return null;
const objName = objNameMatch[1];
if (window[objName] && typeof window[objName] === "object") return window[objName];
try {
const script = document.createElement("script");
script.textContent = scriptContent;
document.head.appendChild(script);
document.head.removeChild(script);
return window[objName];
} catch (error) {
return null;
}
};
const getResponseFromServer = async (path) => {
try {
const response = await fetch(`${window.location.origin}${path}`, {
method: "GET",
headers: { Accept: "text/html" },
credentials: "include",
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return [await response.text(), response.headers.get("content-type")];
} catch (error) {
console.error(`Error fetching ${path}:`, error);
return null;
}
};
// Extract numbered page links (excludes active, next, prev)
const getPaginationLinks = (doc = document) => {
const lis = doc.querySelectorAll("#rewardItemsList .pagination > li:not(.active):not(.next-page):not(.prev-page)");
const uniqueHrefs = new Set();
Array.from(lis).forEach((li) => {
const href = li.querySelector("a")?.getAttribute("href");
if (href) uniqueHrefs.add(href);
});
return Array.from(uniqueHrefs);
};
// Returns the "next >" href if present, else null
const getNextPageLink = (doc = document) => {
return doc.querySelector("#rewardItemsList .pagination > li.next-page a")?.getAttribute("href") || null;
};
// Fetch one rewards page, collect items, return parsed doc
const fetchRewardsPage = async (path) => {
const response = await fetchWithRetry(path);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
doc.querySelectorAll(".reward-item").forEach((el) => {
allItems.push(el);
const script = el.querySelector("script");
if (script) getOrCreateProductObject(script);
});
return doc;
};
// Crawl all paginated pages — parallel within each batch, sequential between batches
const fetchAllRewardsPages = async () => {
const initialLinks = getPaginationLinks(document);
if (!initialLinks.length) return;
const visitedPaths = new Set(initialLinks);
// Fetch first visible batch (pages 2–5) in parallel
const docs = await Promise.all(initialLinks.map((link) => fetchRewardsPage(link)));
await sleep(300);
let lastDoc = docs[docs.length - 1] || document;
while (true) {
const nextLink = getNextPageLink(lastDoc);
if (!nextLink || visitedPaths.has(nextLink)) break;
// Fetch the "next >" pivot page to reveal the next batch
visitedPaths.add(nextLink);
lastDoc = await fetchRewardsPage(nextLink);
await sleep(300);
// Fetch remaining visible pages from that batch in parallel
const batchLinks = getPaginationLinks(lastDoc).filter((l) => !visitedPaths.has(l));
batchLinks.forEach((l) => visitedPaths.add(l));
if (batchLinks.length > 0) {
const batchDocs = await Promise.all(batchLinks.map((link) => fetchRewardsPage(link)));
await sleep(300);
lastDoc = batchDocs[batchDocs.length - 1];
}
}
};
const createCategoryFilterTabs = () => {
const categoryNames = ["all"];
categoryNames.push(...Object.keys(rewardsCategoryFilterOptions));
const filterWrapper = filterSection.querySelector(".category-tab-wrapper");
filterWrapper.addEventListener("click", (e) => {
if (e.target.tagName === "BUTTON") {
addRemoveActive(e.target);
filterProducts();
}
});
categoryNames.forEach((e, index) => {
const newDiv = document.createElement("div");
newDiv.classList = `tab-element category-tab ${index === 0 ? "active" : ""}`;
newDiv.innerHTML = ``;
filterWrapper.append(newDiv);
});
};
const addRemoveActive = (element) => {
const isDelete = element.classList.contains("delete-filter");
const tabWrapper = element.closest(".tab-wrapper");
const tabElement = element.closest(".tab-element");
const activeList =
(tabElement && tabWrapper && tabWrapper.querySelectorAll(":scope > .active")) ||
(isDelete && tabWrapper && tabWrapper.querySelectorAll(":scope > .active")) ||
[];
activeList.forEach((e) => e.classList.remove("active"));
tabElement && tabElement.classList.add("active");
};
const filterProducts = () => {
const category = filterSection.querySelector(".category-tab-wrapper .active button")?.dataset.category || null;
const minPrice = Number(filterSection.querySelector(".points-tab__inputs .points-from")?.value) || 0;
const maxPrice = Number(filterSection.querySelector(".points-tab__inputs .points-to")?.value) || Infinity;
filter.filter({ category, minPrice, maxPrice });
};
const getPointsHistory = async () => {
const pointsHistory = await getResponseFromServer("/api/rewardsactivity?per_page=100");
const historyBody = document.querySelector(".history-wrapper .history-body");
historyBody.replaceChildren();
historyBody.classList.remove("loading");
const historyFooter = document.querySelector(".history-wrapper .history-footer");
historyFooter.replaceChildren();
let pointsData = null;
if (pointsHistory?.[1]?.includes("application/json")) {
try {
pointsData = JSON.parse(pointsHistory[0]);
} catch (error) {
console.error("Failed to parse JSON:", error);
}
}
if (!pointsData?.data?.length) {
const historyLine = document.createElement("div");
historyLine.classList = "history-line history-line__placeholder";
historyLine.innerHTML = `
No rewards history found
You will see you history after your first order
`;
historyBody.append(historyLine);
historyFooter.classList.add("hide");
return;
}
pointsData.data.forEach((element, index) => {
if (index > 20) return;
const historyLine = document.createElement("div");
historyLine.classList = "history-line";
const points =
Number.parseInt(element.points_balance.replaceAll(",", "")) - Number.parseInt(element.points_deducted.replaceAll(",", ""));
historyLine.innerHTML = `
${points > 0 ? "+" : ""}${new Intl.NumberFormat("en-US").format(points)}
${Math.abs(points) === 1 ? "pt" : "pts"}
${element.status}
`;
historyBody.append(historyLine);
});
historyFooter.innerHTML = `View All`;
};
const animateCounter = (element, target, duration = 2000) => {
const start = 0;
let current = start;
const startTime = Date.now();
function update() {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
current = Math.floor(start + (target - start) * eased);
element.textContent = current.toLocaleString();
if (progress < 1) requestAnimationFrame(update);
else element.classList.remove("animating");
}
element.classList.add("animating");
requestAnimationFrame(update);
};
const createCircles = () => {
const evoData = EvoXLayer();
const current = evoData.user.rewards.points;
const pending = evoData.user.rewards.points_pending;
const total = pending < 0 ? current : current + pending;
const curCircle = document.getElementById("current-points-circle");
const pendCircle = document.getElementById("pending-points-circle");
const r = 565.48 * (pending / (current + pending));
curCircle.style.setProperty("--ring-offset", r);
pendCircle.style.setProperty("--ring-offset", 0);
animateCounter(document.querySelector(".points__current .points__text span"), current);
animateCounter(document.querySelector(".points__pending .points__text span"), pending);
animateCounter(document.querySelector(".text-current"), current);
animateCounter(document.querySelector(".text-total"), total);
};
const createRangeSlider = () => {
const $range = document.querySelector(".points-tab-wrapper .points-tab__slider");
const $inputFrom = document.querySelectorAll(".points-tab-wrapper .points-from");
const $inputTo = document.querySelectorAll(".points-tab-wrapper .points-to");
let instance;
const min = 0;
const max = 10000;
let from = 0;
let to = 0;
if ($range && typeof jQuery !== "undefined" && typeof jQuery.fn.ionRangeSlider !== "undefined") {
jQuery($range).ionRangeSlider({
skin: "round",
type: "double",
min,
max,
from: min,
to: max,
step: 50,
prettify_enabled: true,
prettify_separator: ",",
max_postfix: "+",
onStart: updateInputs,
onChange: updateInputs,
onFinish: function (data) {
updateInputs(data);
updateFilter(data);
},
onUpdate: updateFilter,
});
instance = jQuery($range).data("ionRangeSlider");
}
function updateInputs(data) {
from = data.from;
to = data.to;
$inputFrom.forEach((input) => (input.value = from));
$inputTo.forEach((input) => (input.value = to));
}
function updateFilter(data) {
const category = filterSection.querySelector(".category-tab-wrapper .active button").dataset.category;
const minPrice = data.from;
const maxPrice = data.to === data.max ? 99999 : data.to;
filter.filter({ category, minPrice, maxPrice });
}
$inputFrom.forEach((inputFrom) => {
inputFrom.addEventListener("change", function () {
let val = this.value;
if (val < min) val = min;
else if (val > to) val = to;
instance.update({ from: val });
this.value = val;
});
});
$inputTo.forEach((inputTo) => {
inputTo.addEventListener("change", function () {
let val = this.value;
if (val < from) val = from;
else if (val > max) val = max;
instance.update({ to: val });
this.value = val;
});
});
};
// ************************* //
// ===== END FUNCTIONS ===== //
// ===== INIT ===== //
createCircles();
getPointsHistory();
createRangeSlider();
createCategoryFilterTabs();
// Show loader while fetching pages
const loader = document.createElement("div");
loader.className = "rewards-loader";
loader.textContent = "Loading rewards…";
newRewardItemsList.appendChild(loader);
// Collect items already on page 1
allItems.push(...rewardItems);
// Crawl all remaining pages
await fetchAllRewardsPages();
// Remove loader
loader.remove();
// Build filtering reference array
allItems.forEach((el) => {
allItemsFiltering.push({
el,
title: el.querySelector(".product-title")?.textContent.trim() || "",
sku: el.querySelector(".product-details-sku")?.textContent.replace("Product Code", "").trim(),
category: el.querySelector(".product-details-category a")?.textContent.trim() || "",
brand: el.querySelector(".product-details-brand img")?.alt || "",
price: Number(el.querySelector(".product-price")?.textContent.trim() || 0),
disabled: el.querySelector("button")?.hasAttribute("disabled") || false,
});
});
// Reset container and init filter
newRewardItemsList.replaceChildren();
const filter = new VirtualProductFilter(allItems, {
title: rewardItemsTitle,
container: newRewardItemsList,
itemsPerPage: 8,
});
filter.reset();
// Scroll to rewards request handler
document.querySelectorAll('a[href="#rewards-request"]').forEach((link) => {
link.addEventListener("click", function (e) {
e.preventDefault();
if (filter.observer) filter.observer.disconnect();
const scrollToFooter = () => {
const footer = document.getElementById("rewards-request");
if (!footer) return;
window.scrollTo({
top: footer.getBoundingClientRect().top + window.pageYOffset - 120,
behavior: "smooth",
});
let scrollTimeout;
const handleScroll = () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
window.removeEventListener("scroll", handleScroll);
if (filter.observer) {
const sentinel = filter.container.querySelector('[data-sentinel="true"]');
if (sentinel) filter.observer.observe(sentinel);
}
}, 150);
};
window.addEventListener("scroll", handleScroll);
handleScroll();
};
scrollToFooter();
});
});
})();