Wishlist Steal Deal Checker

A user script to check if the wishlist item has best price

// ==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);
})();