Bazaar Item Search powered by IronNerd

View items you are searching for in bazaars!

// ==UserScript==
// @name         Bazaar Item Search powered by IronNerd
// @namespace    [email protected]
// @version      0.6
// @description  View items you are searching for in bazaars!
// @author       Nurv [669537]
// @match        https://www.torn.com/page.php?sid=ItemMarket*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// @license      Copyright IronNerd.me
// @connect      ironnerd.me
// ==/UserScript==

(function () {
    "use strict";

    const BACKEND_URL = "https://www.ironnerd.me";
    let ongoingRequests = new Set();
    let allBazaarItems = [];
    let currentItemData = null;
    let lastUrl = location.href;
    let sortCriteria = [];

    const API = {
        fetchBazaarItems: function (itemID) {
            if (!itemID) return;
            const fullListingsView = document.getElementById("fullListingsView");
            const topCheapestView = document.getElementById("topCheapestView");
            if (ongoingRequests.has(`bazaar_items_${itemID}`)) return;
            ongoingRequests.add(`bazaar_items_${itemID}`);
            fullListingsView.innerHTML = `<p>Loading full listings...</p><div class="loading-spinner"></div>`;
            topCheapestView.innerHTML = `<p>Loading top 3 cheapest items...</p><div class="loading-spinner"></div>`;
            GM_xmlhttpRequest({
                method: "GET",
                url: `${BACKEND_URL}/get_bazaar_items/${itemID}`,
                headers: { Accept: "application/json" },
                onload: function (response) {
                    ongoingRequests.delete(`bazaar_items_${itemID}`);
                    if (response.status === 200) {
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data.bazaar_items) {
                                allBazaarItems = data.bazaar_items;
                                App.renderListings();
                            } else {
                                fullListingsView.innerHTML = `<p>No items found.</p>`;
                                topCheapestView.innerHTML = `<p>No items found.</p>`;
                            }
                        } catch (e) {
                            fullListingsView.innerHTML = `<p>Error parsing server response.</p>`;
                            topCheapestView.innerHTML = `<p>Error parsing server response.</p>`;
                            console.error("Error parsing bazaar items response:", e);
                        }
                    } else {
                        fullListingsView.innerHTML = `<p>Error: ${response.status} - ${response.statusText}</p>`;
                        topCheapestView.innerHTML = `<p>Error: ${response.status} - ${response.statusText}</p>`;
                    }
                },
                onerror: function (error) {
                    ongoingRequests.delete(`bazaar_items_${itemID}`);
                    fullListingsView.innerHTML = `<p>Network error occurred. Please try again later.</p>`;
                    topCheapestView.innerHTML = `<p>Network error occurred. Please try again later.</p>`;
                    console.error("Network error (bazaar items):", error);
                },
            });
        },
    };

    const Util = {
        createCellWithLink: function (url, text) {
            const td = document.createElement("td");
            const a = document.createElement("a");
            a.href = url;
            a.innerText = text;
            a.target = "_blank";
            a.style.color = "#007bff";
            a.style.textDecoration = "none";
            a.addEventListener("mouseover", () => {
                a.style.textDecoration = "underline";
            });
            a.addEventListener("mouseout", () => {
                a.style.textDecoration = "none";
            });
            td.appendChild(a);
            return td;
        },
        createCell: function (content) {
            const td = document.createElement("td");
            td.innerText = content;
            return td;
        },
        createCellWithImage: function (src, alt) {
            const td = document.createElement("td");
            const img = document.createElement("img");
            img.src = src;
            img.alt = alt;
            img.style.height = "30px";
            img.setAttribute("loading", "lazy");
            td.appendChild(img);
            return td;
        },
        formatTimestamp: function (unixTime) {
            if (unixTime.toString().length === 10) {
                unixTime *= 1000;
            }
            const date = new Date(unixTime);
            const now = new Date();
            const diff = Math.floor((now - date) / 1000);
            if (diff < 60) return diff + "s ago";
            const minutes = Math.floor(diff / 60);
            if (minutes < 60) return minutes + "m ago";
            const hours = Math.floor(minutes / 60);
            if (hours < 24) return hours + "h ago";
            const days = Math.floor(hours / 24);
            return days + "d ago";
        },
    };

    const UI = {
        injectAdditionalStyles: function () {
            const style = document.createElement("style");
            style.type = "text/css";
            style.innerHTML = `
:root {
  --primary-bg: #ffffff;
  --primary-color: #000000;
  --border-color: #ddd;
  --table-header-bg: #f2f2f2;
  --nav-bg: #f2f2f2;
  --nav-text: #000;
  --button-padding: 5px 10px;
  --font-family: Arial, sans-serif;
}

.dark-mode {
  --primary-bg: rgba(0,0,0,0.6);
  --primary-color: #f0f0f0;
  --border-color: rgba(255,255,255,0.1);
  --table-header-bg: #333;
  --nav-bg: #444;
  --nav-text: #fff;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

#bazaar-enhancer-container {
  background-color: var(--primary-bg);
  color: var(--primary-color);
  border: 1px solid var(--border-color);
  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
  border-radius: 8px;
  padding: 10px;
  margin: 10px 0;
  transition: background-color 0.3s, color 0.3s;
  font-family: var(--font-family);
}

#showBazaarModal table.bazaar-table,
#bazaar-enhancer-container table.top-cheapest-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 10px;
  table-layout: auto;
}

#showBazaarModal table.bazaar-table th,
#showBazaarModal table.bazaar-table td,
#bazaar-enhancer-container table.top-cheapest-table th,
#bazaar-enhancer-container table.top-cheapest-table td {
  text-align: center;
  padding: 8px;
  border: 1px solid var(--border-color);
  color: var(--primary-color);
}

#showBazaarModal table.bazaar-table th,
#bazaar-enhancer-container table.top-cheapest-table th {
  background-color: var(--table-header-bg);
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.3s;
}

table.bazaar-table tr:hover,
#bazaar-enhancer-container table.top-cheapest-table tr:hover {
  background-color: var(--nav-bg);
}

#showBazaarModal .loading-spinner,
#bazaar-enhancer-container .loading-spinner {
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  width: 24px;
  height: 24px;
  animation: spin 2s linear infinite;
  display: inline-block;
  margin-left: 10px;
}

#bazaar-enhancer-container a.visited-link,
#bazaar-enhancer-container table a.visited-link,
#showBazaarModal a.visited-link,
#showBazaarModal table a.visited-link {
  color: purple !important;
}

#bazaar-nav {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-bottom: 15px;
}

#bazaar-nav button {
  margin: 0 5px;
  padding: var(--button-padding);
  cursor: pointer;
  background-color: var(--nav-bg);
  color: var(--nav-text);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  font-size: 14px;
  transition: background-color 0.3s, transform 0.2s;
}

#bazaar-nav button:hover {
  background-color: var(--table-header-bg);
  transform: scale(1.02);
}

#topCheapestView, #fullListingsView, #filtersView {
  width: 100%;
  box-sizing: border-box;
  padding: 10px;
  background-color: var(--primary-bg);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  margin-bottom: 15px;
}

#fullListingsView {
  height: 500px;
  overflow-y: auto;
}

.filter-form {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px;
  padding: 15px;
  border: 1px solid var(--border-color);
  border-radius: 4px;
  background-color: var(--primary-bg);
  margin-bottom: 15px;
}

.filter-group {
  display: flex;
  flex-direction: column;
}

.filter-group label {
  margin-top: 5px;
  margin-bottom: 5px;
  font-weight: bold;
}

.filter-group input {
  padding: 5px;
  border: 1px solid var(--border-color);
  border-radius: 4px;
  background-color: #cdcaca;
}

.filter-form .filter-buttons {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-top: 10px;
}

.filter-form .filter-buttons button {
  padding: var(--button-padding);
  cursor: pointer;
  background-color: var(--nav-bg);
  color: var(--nav-text);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  font-size: 14px;
  transition: background-color 0.3s, transform 0.2s;
}

.filter-form .filter-buttons button:hover {
  background-color: var(--table-header-bg);
  transform: scale(1.02);
}

#rating-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0,0,0,0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}
#rating-modal {
  background-color: var(--primary-bg);
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 4px 8px rgba(0,0,0,0.3);
  min-width: 300px;
  font-family: var(--font-family);
}

.rating-label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}
.rating-input {
  width: 100%;
  padding: 5px;
  border: 1px solid var(--border-color);
  border-radius: 4px;
  background-color: var(--primary-bg);
  color: var(--primary-color);
}

.rating-btn {
  padding: var(--button-padding);
  cursor: pointer;
  background-color: var(--nav-bg);
  color: var(--nav-text);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  font-size: 14px;
  transition: background-color 0.3s, transform 0.2s;
}
.rating-btn:hover {
  background-color: var(--table-header-bg);
  transform: scale(1.02);
}

@media only screen and (max-width: 600px) {
  #bazaar-enhancer-container table.top-cheapest-table th,
  #bazaar-enhancer-container table.top-cheapest-table td {
    padding: 4px !important;
    font-size: 12px !important;
  }

  #bazaar-enhancer-container table.top-cheapest-table th {
    font-size: 12px !important;
  }
}

            `;
            document.head.appendChild(style);
        },
        ensureContainer: function () {
            return new Promise((resolve) => {
                if (document.querySelector(".captcha-container")) {
                    return;
                }
                let container = document.getElementById("bazaar-enhancer-container");
                if (container) {
                    return resolve(container);
                }
                container = document.createElement("div");
                container.id = "bazaar-enhancer-container";
                container.style.overflow = "hidden";
                let target = document.querySelector(".delimiter___zFh2E");
                if (target && target.parentNode) {
                    target.parentNode.insertBefore(container, target.nextSibling);
                    return resolve(container);
                }
                const observer = new MutationObserver((mutations, obs) => {
                    if (document.querySelector(".captcha-container")) {
                        obs.disconnect();
                        return;
                    }
                    target = document.querySelector(".delimiter___zFh2E");
                    if (target && target.parentNode) {
                        target.parentNode.insertBefore(container, target.nextSibling);
                        obs.disconnect();
                        return resolve(container);
                    }
                });
                observer.observe(document.body, { childList: true, subtree: true });
            });
        },
        displayFiltersView: function () {
            const filtersDiv = document.createElement("div");
            filtersDiv.id = "filtersView";
            filtersDiv.className = "filter-form";
            filtersDiv.innerHTML = `
<div class="filter-group">
  <label for="filter-min-price">Min Price:</label>
  <input type="number" id="filter-min-price" placeholder="e.g., 1000">
</div>
<div class="filter-group">
  <label for="filter-max-price">Max Price:</label>
  <input type="number" id="filter-max-price" placeholder="e.g., 10000">
</div>
<div class="filter-group">
  <label for="filter-min-quantity">Min Qty:</label>
  <input type="number" id="filter-min-quantity" placeholder="e.g., 1">
</div>
<div class="filter-group">
  <label for="filter-max-quantity">Max Qty:</label>
  <input type="number" id="filter-max-quantity" placeholder="e.g., 100">
</div>
<div class="filter-group">
  <label for="filter-min-rating">Min Seller Rating:</label>
  <input type="number" id="filter-min-rating" placeholder="1-5" min="1" max="5" step="0.1">
</div>
<div class="filter-buttons">
  <button id="apply-filters-btn">Apply Filters</button>
  <button id="clear-filters-btn">Clear All Filters</button>
</div>
`;
            filtersDiv
                .querySelector("#apply-filters-btn")
                .addEventListener("click", () => {
                App.filterCriteria.minPrice =
                    parseFloat(document.getElementById("filter-min-price").value) ||
                    null;
                App.filterCriteria.maxPrice =
                    parseFloat(document.getElementById("filter-max-price").value) ||
                    null;
                App.filterCriteria.minQuantity =
                    parseFloat(document.getElementById("filter-min-quantity").value) ||
                    null;
                App.filterCriteria.maxQuantity =
                    parseFloat(document.getElementById("filter-max-quantity").value) ||
                    null;
                App.filterCriteria.minRating =
                    parseFloat(document.getElementById("filter-min-rating").value) ||
                    null;
                GM_setValue("bazaar_filters", App.filterCriteria);
                UI.updateFiltersButtonStyle();
                App.renderListings();
            });
            filtersDiv
                .querySelector("#clear-filters-btn")
                .addEventListener("click", () => {
                App.filterCriteria = {
                    minPrice: null,
                    maxPrice: null,
                    minQuantity: null,
                    maxQuantity: null,
                    minRating: null,
                };
                GM_setValue("bazaar_filters", App.filterCriteria);
                document.getElementById("filter-min-price").value = "";
                document.getElementById("filter-max-price").value = "";
                document.getElementById("filter-min-quantity").value = "";
                document.getElementById("filter-max-quantity").value = "";
                document.getElementById("filter-min-rating").value = "";
                UI.updateFiltersButtonStyle();
                App.renderListings();
            });
            return filtersDiv;
        },
        initTabbedInterface: function (container) {
            container.innerHTML = "";
            const nav = document.createElement("div");
            nav.id = "bazaar-nav";
            nav.style.display = "flex";
            nav.style.justifyContent = "center";
            nav.style.marginBottom = "10px";

            const btnTopCheapest = document.createElement("button");
            btnTopCheapest.innerText = "Top 3";
            btnTopCheapest.addEventListener("click", () => {
                UI.setActiveTab(0);
            });

            const btnFullListings = document.createElement("button");
            btnFullListings.innerText = "All Bazaars";
            btnFullListings.addEventListener("click", () => {
                UI.setActiveTab(1);
            });

            const btnFilters = document.createElement("button");
            btnFilters.innerText = "Filters";
            btnFilters.id = "filters-btn";
            btnFilters.addEventListener("click", () => {
                UI.setActiveTab(2);
            });
            UI.updateFiltersButtonStyle(btnFilters);

            nav.appendChild(btnTopCheapest);
            nav.appendChild(btnFullListings);
            nav.appendChild(btnFilters);
            container.appendChild(nav);

            const topCheapestView = document.createElement("div");
            topCheapestView.id = "topCheapestView";
            topCheapestView.style.padding = "10px";
            topCheapestView.innerHTML = "<p>Loading top cheapest listings...</p>";

            const fullListingsView = document.createElement("div");
            fullListingsView.id = "fullListingsView";
            fullListingsView.style.padding = "10px";
            fullListingsView.style.maxHeight = "350px";
            fullListingsView.style.height = "auto";
            fullListingsView.style.overflowY = "auto";
            fullListingsView.innerHTML = "<p>Loading full listings...</p>";

            const filtersView = UI.displayFiltersView();

            container.appendChild(topCheapestView);
            container.appendChild(fullListingsView);
            container.appendChild(filtersView);

            UI.setActiveTab(0);
        },
        setActiveTab: function (tabIndex) {
            const topCheapestView = document.getElementById("topCheapestView");
            const fullListingsView = document.getElementById("fullListingsView");
            const filtersView = document.getElementById("filtersView");
            if (tabIndex === 0) {
                topCheapestView.style.display = "block";
                fullListingsView.style.display = "none";
                filtersView.style.display = "none";
            } else if (tabIndex === 1) {
                topCheapestView.style.display = "none";
                fullListingsView.style.display = "block";
                filtersView.style.display = "none";
            } else if (tabIndex === 2) {
                topCheapestView.style.display = "none";
                fullListingsView.style.display = "none";
                filtersView.style.display = "block";
                document.getElementById("filter-min-price").value =
                    App.filterCriteria.minPrice || "";
                document.getElementById("filter-max-price").value =
                    App.filterCriteria.maxPrice || "";
                document.getElementById("filter-min-quantity").value =
                    App.filterCriteria.minQuantity || "";
                document.getElementById("filter-max-quantity").value =
                    App.filterCriteria.maxQuantity || "";
                document.getElementById("filter-min-rating").value =
                    App.filterCriteria.minRating || "";
            }
        },
        updateFiltersButtonStyle: function (buttonEl) {
            const btn = buttonEl || document.getElementById("filters-btn");
            const active =
                  App.filterCriteria.minPrice !== null ||
                  App.filterCriteria.maxPrice !== null ||
                  App.filterCriteria.minQuantity !== null ||
                  App.filterCriteria.maxQuantity !== null ||
                  App.filterCriteria.minRating !== null;
            if (active) {
                btn.style.backgroundColor = "lightgreen";
            } else {
                btn.style.backgroundColor = "var(--nav-bg)";
            }
        },
        createSortableHeader: function (text, key) {
            const th = document.createElement("th");
            th.innerText = text;
            th.style.border = "1px solid var(--border-color)";
            th.style.padding = "8px";
            th.style.backgroundColor = "var(--table-header-bg)";
            th.style.textAlign = "center";
            th.style.fontSize = "14px";
            th.style.cursor = "pointer";
            th.addEventListener("click", (e) => {
                if (e.shiftKey) {
                    let existing = sortCriteria.find((crit) => crit.key === key);
                    if (existing) {
                        existing.order = existing.order === "asc" ? "desc" : "asc";
                    } else {
                        sortCriteria.push({ key: key, order: "asc" });
                    }
                } else {
                    let existing = sortCriteria.find((crit) => crit.key === key);
                    if (existing) {
                        existing.order = existing.order === "asc" ? "desc" : "asc";
                        sortCriteria = [existing];
                    } else {
                        sortCriteria = [{ key: key, order: "asc" }];
                    }
                }
                App.renderListings();
            });
            return th;
        },
        displayFullListings: function (items) {
            const fullListingsView = document.getElementById("fullListingsView");
            fullListingsView.innerHTML = "";
            if (items.length === 0) {
                fullListingsView.innerHTML = `<p>No items found.</p>`;
                return;
            }
            const title = document.createElement("h3");
            title.innerText = `Full Listings`;
            title.style.textAlign = "center";
            title.style.marginTop = "2px";
            title.style.marginBottom = "10px";
            fullListingsView.appendChild(title);

            const tableContainer = document.createElement("div");
            tableContainer.style.overflowX = "auto";
            tableContainer.style.width = "100%";

            const table = document.createElement("table");
            table.className = "top-cheapest-table";
            table.style.width = "100%";
            table.style.borderCollapse = "collapse";

            const thead = document.createElement("thead");
            const headerRow = document.createElement("tr");
            headerRow.appendChild(UI.createSortableHeader("Price ($) ↑↓", "price"));
            headerRow.appendChild(UI.createSortableHeader("Quantity ↑↓", "quantity"));
            headerRow.appendChild(
                UI.createSortableHeader("Updated ↑↓", "last_updated")
            );
            headerRow.appendChild(Util.createCell("Seller"));
            headerRow.appendChild(Util.createCell("Rating"));
            thead.appendChild(headerRow);
            table.appendChild(thead);

            const tbody = document.createElement("tbody");
            items.forEach((item) => {
                const tr = document.createElement("tr");
                tr.appendChild(
                    Util.createCellWithLink(
                        `https://www.torn.com/bazaar.php?userID=${item.user_id}`,
                        `$${item.price.toLocaleString()}`
                    )
                );
                tr.appendChild(Util.createCell(item.quantity));
                tr.appendChild(
                    Util.createCell(Util.formatTimestamp(item.last_updated))
                );
                const sellerText = item.player_name ? item.player_name : item.user_id;
                tr.appendChild(
                    Util.createCellWithLink(
                        `https://www.torn.com/profiles.php?XID=${item.user_id}`,
                        sellerText
                    )
                );
                const ratingTd = document.createElement("td");
                if (item.seller_rating && item.seller_rating.avg_rating !== null) {
                    ratingTd.innerText = `${item.seller_rating.avg_rating}/5 (${item.seller_rating.rating_count})`;
                } else {
                    ratingTd.innerText = "Not Rated";
                }
                ratingTd.style.cursor = "pointer";
                ratingTd.title = "Click to rate seller";
                ratingTd.addEventListener("click", () => {
                    UI.openRatingModal(item.user_id, sellerText);
                });
                tr.appendChild(ratingTd);
                tbody.appendChild(tr);
            });
            table.appendChild(tbody);
            tableContainer.appendChild(table);
            fullListingsView.appendChild(tableContainer);
            UI.adjustUnifiedTableTheme();
        },
        displayTopCheapestItems: function (items, itemName) {
            const topCheapestView = document.getElementById("topCheapestView");
            topCheapestView.innerHTML = "";
            if (!items || items.length === 0) {
                topCheapestView.innerHTML = `<p>No items found.</p>`;
                return;
            }
            const title = document.createElement("h3");
            title.innerText = `Top 3 Cheapest ${itemName} Bazaar Items`;
            title.style.textAlign = "center";
            title.style.marginTop = "2px";
            title.style.marginBottom = "10px";
            topCheapestView.appendChild(title);

            const tableContainer = document.createElement("div");
            tableContainer.style.overflowX = "auto";
            tableContainer.style.width = "100%";

            const table = document.createElement("table");
            table.className = "top-cheapest-table";
            table.style.width = "100%";
            table.style.borderCollapse = "collapse";

            const thead = document.createElement("thead");
            const headerRow = document.createElement("tr");
            headerRow.appendChild(UI.createSortableHeader("Price ($) ↑↓", "price"));
            headerRow.appendChild(UI.createSortableHeader("Quantity ↑↓", "quantity"));
            headerRow.appendChild(
                UI.createSortableHeader("Updated ↑↓", "last_updated")
            );
            headerRow.appendChild(Util.createCell("Seller"));
            headerRow.appendChild(Util.createCell("Rating"));
            thead.appendChild(headerRow);
            table.appendChild(thead);

            const tbody = document.createElement("tbody");
            items.slice(0, 3).forEach((item) => {
                const tr = document.createElement("tr");
                tr.appendChild(
                    Util.createCellWithLink(
                        `https://www.torn.com/bazaar.php?userID=${item.user_id}`,
                        `$${item.price.toLocaleString()}`
                    )
                );
                const quantityTd = document.createElement("td");
                quantityTd.innerText = item.quantity;
                quantityTd.style.border = "1px solid var(--border-color)";
                quantityTd.style.padding = "6px";
                quantityTd.style.textAlign = "center";
                quantityTd.style.fontSize = "14px";
                tr.appendChild(quantityTd);
                const updatedTd = document.createElement("td");
                updatedTd.innerText = Util.formatTimestamp(item.last_updated);
                updatedTd.style.border = "1px solid var(--border-color)";
                updatedTd.style.padding = "6px";
                updatedTd.style.textAlign = "center";
                updatedTd.style.fontSize = "14px";
                tr.appendChild(updatedTd);
                const sellerText = item.player_name ? item.player_name : item.user_id;
                tr.appendChild(
                    Util.createCellWithLink(
                        `https://www.torn.com/profiles.php?XID=${item.user_id}`,
                        sellerText
                    )
                );
                const ratingTd = document.createElement("td");
                if (item.seller_rating && item.seller_rating.avg_rating !== null) {
                    ratingTd.innerText = `${item.seller_rating.avg_rating}/5 (${item.seller_rating.rating_count})`;
                } else {
                    ratingTd.innerText = "Not Rated";
                }
                ratingTd.style.cursor = "pointer";
                ratingTd.title = "Click to rate seller";
                ratingTd.addEventListener("click", () => {
                    UI.openRatingModal(item.user_id, sellerText);
                });
                tr.appendChild(ratingTd);
                tbody.appendChild(tr);
            });
            table.appendChild(tbody);
            tableContainer.appendChild(table);
            topCheapestView.appendChild(tableContainer);
            UI.adjustUnifiedTableTheme();
        },
        openRatingModal: function (seller_id, seller_name) {
            const overlay = document.createElement("div");
            overlay.id = "rating-overlay";
            const modal = document.createElement("div");
            modal.id = "rating-modal";
            modal.addEventListener("click", function (e) {
                e.stopPropagation();
            });

            const title = document.createElement("h3");
            title.innerText = `Rate Seller: ${seller_name}`;
            title.style.marginTop = "0";
            modal.appendChild(title);

            const starsDiv = document.createElement("div");
            starsDiv.style.marginBottom = "10px";
            starsDiv.style.fontSize = "24px";
            starsDiv.style.cursor = "pointer";
            let selectedRating = 0;
            for (let i = 1; i <= 5; i++) {
                const star = document.createElement("span");
                star.innerText = "☆";
                star.dataset.value = i;
                star.addEventListener("click", function () {
                    selectedRating = parseInt(this.dataset.value);
                    const allStars = starsDiv.querySelectorAll("span");
                    allStars.forEach((s) => {
                        s.innerText =
                            parseInt(s.dataset.value) <= selectedRating ? "★" : "☆";
                    });
                });
                starsDiv.appendChild(star);
            }
            modal.appendChild(starsDiv);

            const apiKeyDiv = document.createElement("div");
            apiKeyDiv.style.marginBottom = "10px";
            const apiKeyLabel = document.createElement("label");
            apiKeyLabel.innerText = "Your API Key: ";
            apiKeyLabel.className = "rating-label";
            const apiKeyInput = document.createElement("input");
            apiKeyInput.type = "text";
            apiKeyInput.className = "rating-input";
            apiKeyInput.placeholder = "Enter your Torn API key";
            apiKeyInput.value = GM_getValue("buyer_api_key", "");
            apiKeyDiv.appendChild(apiKeyLabel);
            apiKeyDiv.appendChild(apiKeyInput);
            modal.appendChild(apiKeyDiv);

            const messageDiv = document.createElement("div");
            messageDiv.style.marginBottom = "10px";
            messageDiv.style.textAlign = "center";
            modal.appendChild(messageDiv);

            const btnContainer = document.createElement("div");
            btnContainer.style.textAlign = "right";
            btnContainer.style.marginTop = "15px";

            const submitBtn = document.createElement("button");
            submitBtn.innerText = "Submit";
            submitBtn.className = "rating-btn";
            submitBtn.style.marginRight = "10px";
            submitBtn.addEventListener("click", function () {
                if (selectedRating === 0) {
                    messageDiv.style.color = "red";
                    messageDiv.innerText = "Please select a rating.";
                    return;
                }
                const apiKey = apiKeyInput.value.trim();
                if (!apiKey) {
                    messageDiv.style.color = "red";
                    messageDiv.innerText = "Please enter your API key.";
                    return;
                }
                GM_setValue("buyer_api_key", apiKey);
                const payload = {
                    seller_id: seller_id,
                    api_key: apiKey,
                    rating: selectedRating,
                };
                GM_xmlhttpRequest({
                    method: "POST",
                    url: `${BACKEND_URL}/v1/api/bazaar/rate_seller`,
                    headers: { "Content-Type": "application/json" },
                    data: JSON.stringify(payload),
                    onload: function (response) {
                        if (response.status === 200) {
                            messageDiv.style.color = "green";
                            messageDiv.innerText = "Rating submitted successfully!";
                            App.renderListings();
                        } else {
                            messageDiv.style.color = "red";
                            messageDiv.innerText = "Error: " + response.responseText;
                        }
                        setTimeout(() => {
                            if (document.body.contains(overlay)) {
                                document.body.removeChild(overlay);
                            }
                        }, 1500);
                    },
                    onerror: function (error) {
                        messageDiv.style.color = "red";
                        messageDiv.innerText = "Network error. Please try again later.";
                        setTimeout(() => {
                            if (document.body.contains(overlay)) {
                                document.body.removeChild(overlay);
                            }
                        }, 1500);
                    },
                });
            });
            btnContainer.appendChild(submitBtn);

            const cancelBtn = document.createElement("button");
            cancelBtn.innerText = "Cancel";
            cancelBtn.className = "rating-btn";
            cancelBtn.addEventListener("click", function () {
                if (document.body.contains(overlay)) {
                    document.body.removeChild(overlay);
                }
            });
            btnContainer.appendChild(cancelBtn);

            modal.appendChild(btnContainer);
            overlay.appendChild(modal);
            document.body.appendChild(overlay);

            overlay.addEventListener("click", function (e) {
                if (e.target === overlay && document.body.contains(overlay)) {
                    document.body.removeChild(overlay);
                }
            });

            document.addEventListener("keydown", function escHandler(e) {
                if (e.key === "Escape") {
                    if (document.body.contains(overlay)) {
                        document.body.removeChild(overlay);
                    }
                    document.removeEventListener("keydown", escHandler);
                }
            });
        },

        adjustUnifiedTableTheme: function () {
            const isDarkMode = document.body.classList.contains("dark-mode");
            const tables = document.querySelectorAll(".top-cheapest-table");
            tables.forEach((table) => {
                if (isDarkMode) {
                    table.style.backgroundColor = "#1c1c1c";
                    table.style.color = "#f0f0f0";
                    table.querySelectorAll("th").forEach((th) => {
                        th.style.backgroundColor = "#444";
                        th.style.color = "#ffffff";
                    });
                    table.querySelectorAll("tr:nth-child(even)").forEach((tr) => {
                        tr.style.backgroundColor = "#2a2a2a";
                    });
                    table.querySelectorAll("tr:nth-child(odd)").forEach((tr) => {
                        tr.style.backgroundColor = "#1e1e1e";
                    });
                    table.querySelectorAll("td a").forEach((a) => {
                        a.style.color = "#4ea8de";
                    });
                } else {
                    table.style.backgroundColor = "#fff";
                    table.style.color = "#000";
                    table.querySelectorAll("th").forEach((th) => {
                        th.style.backgroundColor = "var(--table-header-bg)";
                        th.style.color = "#000";
                    });
                    table.querySelectorAll("tr:nth-child(even)").forEach((tr) => {
                        tr.style.backgroundColor = "#f9f9f9";
                    });
                    table.querySelectorAll("tr:nth-child(odd)").forEach((tr) => {
                        tr.style.backgroundColor = "#fff";
                    });
                    table.querySelectorAll("td a").forEach((a) => {
                        a.style.color = "#007bff";
                    });
                }
            });
        },
        adjustBazaarEnhancerContainerTheme: function () {
            const container = document.getElementById("bazaar-enhancer-container");
            const isDarkMode = document.body.classList.contains("dark-mode");
            if (container) {
                if (isDarkMode) {
                    container.style.backgroundColor = "rgba(0,0,0,0.6)";
                    container.style.color = "#f0f0f0";
                    container.style.border = "1px solid rgba(255,255,255,0.1)";
                    container.style.boxShadow = "0 4px 8px rgba(0,0,0,0.4)";
                } else {
                    container.style.backgroundColor = "#ffffff";
                    container.style.color = "#000000";
                    container.style.border = "1px solid #ddd";
                    container.style.boxShadow = "0 4px 8px rgba(0,0,0,0.1)";
                }
            }
        },
        observeDarkMode: function () {
            const observer = new MutationObserver(() => {
                UI.adjustUnifiedTableTheme();
                UI.adjustBazaarEnhancerContainerTheme();
            });
            observer.observe(document.body, {
                attributes: true,
                attributeFilter: ["class"],
            });
        },
    };

    const App = {
        filterCriteria: GM_getValue("bazaar_filters", {
            minPrice: null,
            maxPrice: null,
            minQuantity: null,
            maxQuantity: null,
            minRating: null,
        }),
        getItemInfoFromURL: function () {
            const url = new URL(window.location.href);
            let itemID = null;
            let itemName = "";
            if (url.hash) {
                let hash = url.hash.startsWith("#/")
                ? url.hash.substring(2)
                : url.hash.substring(1);
                let params = new URLSearchParams(hash);
                itemID = params.get("itemID");
                itemName = decodeURIComponent(params.get("itemName") ?? "");
            }
            if (!itemID) {
                let params = url.searchParams;
                itemID = params.get("itemID");
                itemName = decodeURIComponent(params.get("itemName") ?? "");
            }
            return {
                itemID: itemID ? parseInt(itemID, 10) : null,
                itemName: itemName,
            };
        },
        clearListingsData: function () {
            const topCheapestView = document.getElementById("topCheapestView");
            const fullListingsView = document.getElementById("fullListingsView");
            if (topCheapestView) {
                topCheapestView.innerHTML = `<p>No item selected.</p>`;
            }
            if (fullListingsView) {
                fullListingsView.innerHTML = `<p>No item selected.</p>`;
            }
        },
        applyFilters: function (items) {
            return items.filter((item) => {
                if (
                    App.filterCriteria.minPrice !== null &&
                    item.price < App.filterCriteria.minPrice
                )
                    return false;
                if (
                    App.filterCriteria.maxPrice !== null &&
                    item.price > App.filterCriteria.maxPrice
                )
                    return false;
                if (
                    App.filterCriteria.minQuantity !== null &&
                    item.quantity < App.filterCriteria.minQuantity
                )
                    return false;
                if (
                    App.filterCriteria.maxQuantity !== null &&
                    item.quantity > App.filterCriteria.maxQuantity
                )
                    return false;
                if (App.filterCriteria.minRating !== null) {
                    if (
                        !item.seller_rating ||
                        item.seller_rating.avg_rating === null ||
                        item.seller_rating.avg_rating < App.filterCriteria.minRating
                    ) {
                        return false;
                    }
                }
                return true;
            });
        },
        multiSort: function (a, b) {
            for (let crit of sortCriteria) {
                const key = crit.key;
                const order = crit.order;
                if (a[key] < b[key]) return order === "asc" ? -1 : 1;
                if (a[key] > b[key]) return order === "asc" ? 1 : -1;
            }
            return 0;
        },
        renderListings: function () {
            if (currentItemData && allBazaarItems.length > 0) {
                let filteredItems = App.applyFilters(allBazaarItems);
                let sortedItems = filteredItems.sort(App.multiSort);
                UI.displayFullListings(sortedItems);
                UI.displayTopCheapestItems(
                    sortedItems.slice(0, 3),
                    currentItemData.itemName
                );
            }
        },
        init: function () {
            UI.injectAdditionalStyles();
            UI.ensureContainer().then((container) => {
                UI.initTabbedInterface(container);
                UI.observeDarkMode();
                const info = App.getItemInfoFromURL();
                if (info.itemID) {
                    currentItemData = info;
                    API.fetchBazaarItems(info.itemID);
                } else {
                    App.clearListingsData();
                }
                UI.adjustBazaarEnhancerContainerTheme();
            });
        },
        checkForItems: function (wrapper) {
            if (!wrapper || wrapper.id === "bazaar-enhancer-container") return;
            let itemTile = wrapper.previousElementSibling;
            if (itemTile && itemTile.id === "bazaar-enhancer-container") {
                itemTile = itemTile.previousElementSibling;
            }
            if (!itemTile) return;
            const nameEl = itemTile.querySelector(".name___ukdHN");
            const btn = itemTile.querySelector(
                'button[aria-controls^="wai-itemInfo-"]'
            );
            if (nameEl && btn) {
                const itemName = nameEl.textContent.trim();
                const idParts = btn.getAttribute("aria-controls").split("-");
                const itemId = idParts[idParts.length - 1];
                currentItemData = {
                    itemID: parseInt(itemId, 10),
                    itemName: itemName,
                };
                API.fetchBazaarItems(currentItemData.itemID);
            }
        },
        checkForItemsMobile: function () {
            if (window.innerWidth >= 784) return;
            const sellerList = document.querySelector("ul.sellerList___e4C9_");
            const headerEl = document.querySelector(
                ".itemsHeader___ZTO9r .title___ruNCT"
            );
            const itemName = headerEl ? headerEl.textContent.trim() : "Unknown";
            const btn = document.querySelector(
                '.itemsHeader___ZTO9r button[aria-controls^="wai-itemInfo-"]'
            );
            let itemId = null;
            if (btn) {
                const parts = btn.getAttribute("aria-controls").split("-");
                itemId =
                    parts.length > 2 ? parts[parts.length - 2] : parts[parts.length - 1];
            }
            if (!itemId) return;
            currentItemData = {
                itemID: parseInt(itemId, 10),
                itemName: itemName,
            };
            API.fetchBazaarItems(currentItemData.itemID);
        },
        observeMutations: function () {
            const observer = new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    mutation.addedNodes.forEach((node) => {
                        if (node.nodeType !== Node.ELEMENT_NODE) return;
                        if (
                            window.innerWidth < 784 &&
                            node.classList.contains("sellerList___e4C9_")
                        ) {
                            App.checkForItemsMobile();
                        } else if (
                            window.innerWidth >= 784 &&
                            node.className.includes("sellerListWrapper")
                        ) {
                            App.checkForItems(node);
                        }
                    });
                });
            });
            observer.observe(document.body, { childList: true, subtree: true });
        },
    };

    App.observeMutations();

    setInterval(() => {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            setTimeout(() => {
                let info = App.getItemInfoFromURL();
                if (info.itemID) {
                    currentItemData = info;
                    API.fetchBazaarItems(info.itemID);
                } else {
                    if (window.innerWidth < 784) {
                        App.checkForItemsMobile();
                    } else {
                        const wrapper = document.querySelector(
                            '[class*="sellerListWrapper"]'
                        );
                        if (wrapper) App.checkForItems(wrapper);
                        else {
                            App.clearListingsData();
                            currentItemData = null;
                        }
                    }
                }
            }, 100);
        }
    }, 500);

    App.init();

    function varFallback(variable, fallback) {
        try {
            return (
                getComputedStyle(document.documentElement)
                .getPropertyValue(variable)
                .trim() || fallback
            );
        } catch (e) {
            return fallback;
        }
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址