(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 = `
Order number: ${element.order_prefix_id}
${element.created_at}
${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; }); }); */