// ==UserScript==
// @name Wishlist Steal Deal Checker
// @namespace https://greasyfork.org/en/users/1019658-aayush-dutt
// @version 0.10
// @description A user script to check if the wishlist item has best price
// @author aayushdutt
// @match https://www.amazon.com/hz/wishlist*
// @match https://www.amazon.in/hz/wishlist*
// @match https://www.amazon.de/hz/wishlist/ls*
// @match https://www.amazon.fr/hz/wishlist/ls*
// @match https://www.amazon.it/hz/wishlist/ls*
// @match https://www.amazon.es/hz/wishlist/ls*
// @match https://www.amazon.nl/hz/wishlist/ls*
// @match https://www.amazon.se/hz/wishlist/ls*
// @match https://www.amazon.co.jp/hz/wishlist/ls*
// @match https://www.amazon.co.uk/hz/wishlist/ls*
// @match https://www.amazon.com.mx/hz/wishlist/ls*
// @match https://www.amazon.com.au/hz/wishlist/ls*
// @match https://www.amazon.com.be/hz/wishlist/ls*
// @grant none
// @link https://greasyfork.org/en/scripts/468955-wishlist-steal-deal-checker
// @license MIT
// ==/UserScript==
(function () {
"use strict";
/**
* @returns {HTMLElement[]} An array of nodes.
*/
const getAllNodes = () => {
return Array.from(document.querySelectorAll("ul#g-items > li[data-id]"));
};
/**
* Parses the nodes and extracts relevant information.
* @returns {Object[]} An array of parsed node objects.
*/
const parseNodes = () => {
/**
* @param {HTMLElement} node - The node element.
* @returns {number} The price value.
*/
const getPrice = (node) => {
return parseInt(node.getAttribute("data-price"));
};
/**
* @param {HTMLElement} node - The node element.
* @returns {HTMLElement|null} The comment node or null if not found.
*/
const getCommentNode = (node) => {
return node.querySelector(
'span[data-csa-c-element-id="list-desktop-wishlist-item-info-cqp-comment"]'
);
};
/**
* @param {HTMLElement|null} node - The comment node.
* @returns {number[]} An array of comment prices.
*/
const getCommentPrices = (node) => {
const commentText = node ? node.innerText.trim() : "";
if (!commentText) return [];
return commentText
.split(" ")
.map((e) => parseInt(e))
.filter((e) => !isNaN(e) && typeof e === "number");
};
/**
* Checks if the given price is the best price among the comment prices.
* @param {number} price - The price to compare.
* @param {number[]} commentPrices - An array of comment prices.
* @returns {number} -1 if bad price, 0 if equal to prev best, 1 if the price beats all comment prices
*/
const getIsBestPrice = (price, commentPrices) => {
if (!commentPrices || !commentPrices.length || !price) return false;
const minCommentPrice = Math.min(...commentPrices);
if (price === minCommentPrice) {
return 0;
}
if (price > minCommentPrice) return -1;
if (price < minCommentPrice) return 1;
};
const nodes = getAllNodes();
return nodes.map((node) => {
const price = getPrice(node);
const commentNode = getCommentNode(node);
const commentPrices = getCommentPrices(commentNode);
const priceComparison = getIsBestPrice(price, commentPrices);
return {
node,
price,
commentPrices,
priceComparison,
commentNode,
};
});
};
/**
* @returns {string}
*/
const getCsrf = () => {
return document.querySelectorAll('input[name="anti-csrftoken-a2z"]')[0]
.value;
};
const csrf = getCsrf();
const updateComment = async (node, updatedComment) => {
const actionParams = JSON.parse(
node.getAttribute("data-reposition-action-params")
);
const body = JSON.stringify({
comment: updatedComment,
desiredQuantity: 1,
isJson: true,
listType: "wishlist",
priority: 0,
hasQuantity: 0,
viewType: "list",
itemID: actionParams.itemExternalId,
listID: node.getAttribute("data-id"),
sid: actionParams.sid,
});
const resp = await fetch("/hz/wishlist/updatecqp", {
headers: {
"content-type": "application/json",
"anti-csrftoken-a2z": csrf,
},
body,
method: "POST",
});
console.log(`Updated comment to: ${updatedComment}`, resp);
// if response not ok then throw error
if (!resp.ok) {
throw new Error(resp.status);
}
};
const addUpdateButton = (item) => {
if (
!item.price ||
(item.commentPrices &&
item.commentPrices.length &&
item.commentPrices.includes(item.price))
)
return;
const handleUpdateButtonClick = async () => {
const updatedPrices = [...item.commentPrices, item.price];
updatedPrices.sort((a, b) => b - a);
const updatedComment = updatedPrices.join(", ");
try {
await updateComment(item.node, updatedComment); // update comment api call
// Handle comment node (reload page, setting innertext doesn't work)
if (!item.commentNode.innerText.trim()) location.reload();
item.commentNode.innerText = updatedComment; // update comment text in dom
} catch (e) {
const errMsg = `Error updating comment to: ${updatedComment}, err=${e.message}`;
console.error(errMsg);
window.alert(errMsg);
}
};
const containerEl = item.node.querySelector(
"div.a-column.a-span12.g-span12when-narrow.g-span5when-wide.g-item-comment.a-span-last"
);
containerEl.classList.remove("a-hidden");
const newButton = document.createElement("button");
newButton.innerText = "Add Current Price";
newButton.className =
"a-button a-button-base a-button-small add-current-price";
newButton.style.padding = "2px 10px";
newButton.style.margin = "10px 0px";
newButton.onclick = handleUpdateButtonClick;
containerEl.appendChild(newButton);
};
/**
* Updates the style of the best price nodes.
* @param {Object[]} wishlistItems
*/
const updateBestNodes = (wishlistItems) => {
wishlistItems.forEach((item, idx) => {
if (item.node.getAttribute("data-isdone") === "1") return;
addUpdateButton(item);
if (item.priceComparison === 0) {
// Equal to prev best
item.node.style.background = "#f1fffe";
} else if (item.priceComparison === 1) {
// Better than prev best
item.node.style.background = "#d0fbe4";
}
item.node.setAttribute("data-isdone", "1");
});
};
/**
* @param {MutationRecord[]} mutationList
* @param {MutationObserver} observer
*/
const callback = (mutationList, observer) => {
console.log("[Wishlist Steal Deal Checker] list changed");
const wishlistItems = parseNodes();
updateBestNodes(wishlistItems);
};
callback([], null);
const targetNode = document.getElementById("g-items");
const config = { childList: true };
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
})();