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