(async () => {
if (!document.querySelector(".ex-rewards")) return;
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 = [];
const categoryFilter = {
office: /office|office supplies|correction|padded|drywipe|clipboards|seats|tape|batteries|tables|highlighters/i,
snacks: /snacks|hot drinks|chocolate/i,
technology:
/technology|monitor|printer|toner|DEMO20OFF|DEMOREBATE05|DEMOREBATE1|DEMOREBATE15|DEMOREBATE2|DEMOACS|DEMOWWP|DEMO20|DEMOSTC|DEMOFA|pads|CBX-1-K/i,
other: /other/,
};
// Apply User name
newRewards.querySelector(".points-info__title span").textContent = EvoXLayer().user.name;
// ===== 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`;
// Extract data ftom DOM elements
this.products = this.extractProductData(allItems);
this.buildIndices();
// Current state
this.filteredProducts = [...this.products];
this.renderedItems = new Set();
this.currentPage = 0;
// Create virtual container
this.setupVirtualContainer();
this.setupIntersectionObserver();
}
extractProductData(elements) {
return elements.map((el, index) => {
return {
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),
};
// Initialize Map with all category groups
Object.keys(categoryFilter).forEach((category) => {
this.indices.byCategory.set(category, []);
});
// Add a default "other" category for unmatched products
this.indices.byCategory.set("other", []);
// Categorise each product
this.products.forEach((product) => {
const categoryName = product.category || "";
const sku = product.sku || "";
let matchedGroup = "other";
// Find product category
for (let [group, regex] of Object.entries(categoryFilter)) {
if (regex.test(categoryName)) {
matchedGroup = group.toLowerCase();
break;
}
if (regex.test(sku)) {
matchedGroup = group.toLowerCase();
break;
}
}
this.indices.byCategory.get(matchedGroup).push(product);
});
}
setupVirtualContainer() {
this.container.innerHTML = "";
// Create spacer for total height
this.spacer = document.createElement("div");
this.spacer.style.height = "0px";
this.container.append(this.spacer);
// Create visible container
this.viewport = document.createElement("div");
this.viewport.classList = "row items-row";
this.container.append(this.viewport);
}
setupIntersectionObserver() {
// Observe when user scrolls near bottom
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;
// Filter by category
if (category !== undefined && category !== null && category !== "all") {
result = this.indices.byCategory.get(category.toLowerCase());
}
// Filter by price
if (minPrice !== undefined || maxPrice !== undefined) {
const min = minPrice || 0;
const max = maxPrice || Infinity;
result = result.filter((p) => p.price >= min && p.price <= max);
}
// Flter by search
if (search) {
const term = search.toLowerCase();
result = result.filter((p) => {
p.title.toLowerCase().includes(term) || p.sku.toLowerCase().includes(term);
});
}
// Clear and re-render
this.filteredProducts = result;
this.currentPage = 0;
this.renderedItems.clear();
this.viewport.innerHTML = "";
this.title.textContent = `${this.filteredProducts.length} ${this.filteredProducts.length === 1 ? "reward" : "rewards"} found`;
//this.updateSpacer();
this.renderBatch(0, this.itemsPerPage);
}
search(term) {
return this.filter({ search: term });
}
reset() {
this.filteredProducts = [...this.products];
this.currentPage = 0;
this.renderedItems.clear();
this.viewport.innerHTML = "";
//this.updateSpacer();
this.renderBatch(0, this.itemsPerPage);
return this.products.length;
}
updateSpacer() {
// Estimate total height (assuming average 200px per item)
const estimatedHeight = this.filteredProducts.length * 200;
this.spacer.style.height = `${estimatedHeight}px`;
}
renderBatch(start, count) {
// add placeholder if no items
if (!this.filteredProducts.length) {
this.viewport.innerHTML = "";
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);
return;
}
// remove any placeholders
this.viewport.querySelectorAll(".reward-item-placeholder").forEach((e) => {
e.remove();
});
const end = Math.min(start + count, this.filteredProducts.length);
for (let i = start; i < end; i++) {
// Check if product is already generated
if (this.renderedItems.has(i)) continue;
const product = this.filteredProducts[i];
const clone = product.element.cloneNode(true);
this.viewport.appendChild(clone);
this.renderedItems.add(i);
setTimeout(() => {
clone.classList.add("reward-item-new");
}, 10);
}
}
loadMoreItems() {
const nextStart = this.renderedItems.size;
if (nextStart < this.filteredProducts.length) {
this.renderBatch(nextStart, this.itemsPerPage);
}
}
}
// *********************** //
// ===== END CLASSES ===== //
// ===== FUNCTIONS ===== //
// ********************* //
// Returs array with links if there is pagination on rewards page
const getPaginationLinks = (list) => {
if (list.length === 0) return [];
// Create set of used hrefs
const uniqueHrefs = new Set();
Array.from(list).forEach((li) => {
const anchor = li.querySelector("a");
if (anchor && anchor.href) {
const href = anchor.getAttribute("href");
if (href) uniqueHrefs.add(href);
}
});
return Array.from(uniqueHrefs);
};
// Checks for obj_XXXXXXX or creates global variable for revard items
const getOrCreateProductObject = (scriptElement) => {
const scriptContent = scriptElement.textContent;
// Extract object name
const objNameMatch = scriptContent.match(/var\s+(obj_\d+)/);
if (!objNameMatch) return null;
// Check if already exists
const objName = objNameMatch[1];
if (window[objName] && typeof window[objName] === "object") {
return window[objName];
}
// Create it globally by injecting script tag
try {
const script = document.createElement("script");
script.textContent = scriptContent;
document.head.appendChild(script);
document.head.removeChild(script);
return window[objName];
} catch (error) {
return null;
}
};
// Get items from respons and appends
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;
}
};
// Create category filter buttons
const createCategoryFilterTabs = () => {
const categoryNames = ["all"];
categoryNames.push(...Object.keys(categoryFilter));
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);
});
};
// Add and remove "active" class
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");
};
// Filter products
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 });
};
// Create Points History
const getPointsHistory = async () => {
// Get History
const pointsHistory = await getResponseFromServer("/api/rewardsactivity?per_page=100");
// clear history body
const historyBody = document.querySelector(".history-wrapper .history-body");
historyBody.innerHTML = "";
historyBody.classList.remove("loading");
// clear history footer
const historyFooter = document.querySelector(".history-wrapper .history-footer");
historyFooter.innerHTML = "";
if (!pointsHistory || !pointsHistory[1].includes("application/json") || JSON.parse(pointsHistory[0]).data.length === 0) {
const historyLine = document.createElement("div");
historyLine.classList = "history-line histoy-line__placeholder";
historyLine.innerHTML = `
No rewards history found
You will see you history after your first order
`;
historyBody.append(historyLine);
return;
}
const pointsData = JSON.parse(pointsHistory[0]);
//append history
pointsData.data.forEach((element, index) => {
if (index > 20) return;
const historyLine = document.createElement("div");
historyLine.classList = "history-line";
const points = parseInt(element.points_balance) - parseInt(element.points_deducted);
historyLine.innerHTML = `
${points > 0 ? "+" : ""}${points}${
Math.abs(points) === 1 ? "pt" : "pts"
}
${
element.status
}
`;
historyBody.append(historyLine);
});
historyFooter.innerHTML = `View All`;
};
// Create circle animation
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);
// Easing function (easeOutCubic)
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);
};
// Initialize points and run animation
const createCircles = () => {
const current = EvoXLayer().user.rewards.points;
const pending = EvoXLayer().user.rewards.points_pending;
const total = 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);
const currentText = document.querySelector(".points__current .points__text span");
const pendingText = document.querySelector(".points__pending .points__text span");
const currentTextCircle = document.querySelector(".text-current");
const totalTextCircle = document.querySelector(".text-total");
animateCounter(currentText, current);
animateCounter(pendingText, pending);
animateCounter(currentTextCircle, current);
animateCounter(totalTextCircle, total);
};
// Ceate range slider
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;
//const customValues = [0, 10, 20, 30, 40, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 10000];
if ($range && typeof jQuery !== "undefined" && typeof jQuery.fn.ionRangeSlider !== "undefined") {
jQuery($range).ionRangeSlider({
skin: "round",
type: "double",
min: min,
max: max,
//values: customValues,
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;
// validate
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;
// validate
if (val < from) {
val = from;
} else if (val > max) {
val = max;
}
instance.update({
to: val,
});
this.value = val;
});
});
};
// ************************* //
// ===== END FUNCTIONS ===== //
// Run circle animation
createCircles();
// Get Points History
getPointsHistory();
// Create range slider
createRangeSlider();
// Create category filter
createCategoryFilterTabs();
// Push all reward items to array
allItems.push(...rewardItems);
// Find any pagination links
const paginationLinks = getPaginationLinks(
document.querySelectorAll("#rewardItemsList .pagination > li:not(.active):not(.next-page):not(.prev-page)")
);
// Send get request if there is pagination
await Promise.all(
paginationLinks.map(async (link) => {
const text = await getResponseFromServer(link);
if (!text || !text[1].includes("text/html")) return;
const parser = new DOMParser();
const doc = parser.parseFromString(text[0], "text/html");
const items = doc.querySelectorAll(".reward-item");
items.forEach((e) => {
allItems.push(e);
//itemsRow.append(e);
const script = e.querySelector("script");
if (script) {
getOrCreateProductObject(script);
}
});
})
);
// Fill Convenient array wit objects for filtering
allItems.forEach((el) => {
const obj = {
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,
};
allItemsFiltering.push(obj);
});
// Reset new-reward-items-list
newRewardItemsList.innerHTML = "";
// Define new instance
const filter = new VirtualProductFilter(allItems, {
title: rewardItemsTitle,
container: newRewardItemsList,
itemsPerPage: 8,
});
// Reset all filters
filter.reset();
})();
/*
const $range = $(".points-tab-wrapper .points-tab__slider");
const $inputFrom = $(".points-tab-wrapper .points-from");
const $inputTo = $(".points-tab-wrapper .points-to");
let instance;
const min = 0;
const max = 10000;
let from = 0;
let to = 0;
//const customValues = [0, 10, 20, 30, 40, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 10000];
$range.ionRangeSlider({
skin: "round",
type: "double",
min: min,
max: max,
//values: customValues,
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 = $range.data("ionRangeSlider");
function updateInputs(data) {
from = data.from;
to = data.to;
$inputFrom.prop("value", from);
$inputTo.prop("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.on("change", function () {
let val = $(this).prop("value");
// validate
if (val < min) {
val = min;
} else if (val > to) {
val = to;
}
instance.update({
from: val,
});
$(this).prop("value", val);
});
$inputTo.on("change", function () {
let val = $(this).prop("value");
// validate
if (val < from) {
val = from;
} else if (val > max) {
val = max;
}
instance.update({
to: val,
});
$(this).prop("value", val);
});
*/
/*
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;
//const customValues = [0, 10, 20, 30, 40, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 10000];
if ($range && typeof jQuery !== 'undefined' && typeof jQuery.fn.ionRangeSlider !== 'undefined') {
jQuery($range).ionRangeSlider({
skin: "round",
type: "double",
min: min,
max: max,
//values: customValues,
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;
// validate
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;
// validate
if (val < from) {
val = from;
} else if (val > max) {
val = max;
}
instance.update({
to: val,
});
this.value = val;
});
});
*/