// ==UserScript==
// @name Bazaar Item Search powered by IronNerd
// @namespace [email protected]
// @version 0.5.1
// @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';
const ongoingRequests = new Set();
let allBazaarItems = [];
let currentItemData = null;
let lastUrl = location.href;
let sortKey = 'price';
let sortOrder = 'asc';
function init() {
injectAdditionalStyles();
ensureBazaarEnhancerContainer().then(container => {
initTabbedInterface(container);
observeDarkMode();
const info = getItemInfoFromURL();
if (info.itemID) {
currentItemData = info;
fetchBazaarItems(info.itemID);
} else {
clearListingsData();
}
adjustBazaarEnhancerContainerTheme();
});
}
function getItemInfoFromURL() {
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
};
}
function clearListingsData() {
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>`;
}
}
function createCellWithLink(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;
}
function createCell(content) {
const td = document.createElement('td');
td.innerText = content;
return td;
}
function createCellWithImage(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;
}
function formatTimestamp(unixTime) {
if (unixTime.toString().length === 10) {
unixTime = 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';
}
function ensureBazaarEnhancerContainer() {
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 });
});
}
function initTabbedInterface(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', () => { setActiveTab(0); });
const btnFullListings = document.createElement('button');
btnFullListings.innerText = 'All Bazaars';
btnFullListings.addEventListener('click', () => { setActiveTab(1); });
nav.appendChild(btnTopCheapest);
nav.appendChild(btnFullListings);
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>';
container.appendChild(topCheapestView);
container.appendChild(fullListingsView);
setActiveTab(0);
}
function setActiveTab(tabIndex) {
const topCheapestView = document.getElementById('topCheapestView');
const fullListingsView = document.getElementById('fullListingsView');
if (tabIndex === 0) {
topCheapestView.style.display = 'block';
fullListingsView.style.display = 'none';
} else {
topCheapestView.style.display = 'none';
fullListingsView.style.display = 'block';
}
}
function sortItems(items) {
return items.slice().sort((a, b) => {
let valA = a[sortKey], valB = b[sortKey];
if (valA < valB) return sortOrder === 'asc' ? -1 : 1;
if (valA > valB) return sortOrder === 'asc' ? 1 : -1;
return 0;
});
}
function renderListings() {
if (currentItemData && allBazaarItems.length > 0) {
const sortedItems = sortItems(allBazaarItems);
const fullListingsView = document.getElementById('fullListingsView');
const topCheapestView = document.getElementById('topCheapestView');
displayFullListings(sortedItems, fullListingsView);
displayTopCheapestItems(sortedItems.slice(0, 3), currentItemData.itemName, topCheapestView);
}
}
function createSortableHeader(text, key) {
const th = document.createElement('th');
th.innerText = text;
th.style.border = '1px solid #ccc';
th.style.padding = '8px';
th.style.backgroundColor = '#e0e0e0';
th.style.textAlign = 'center';
th.style.fontSize = '14px';
th.style.cursor = 'pointer';
th.addEventListener('click', () => {
if (sortKey === key) {
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
} else {
sortKey = key;
sortOrder = 'asc';
}
renderListings();
});
return th;
}
function fetchBazaarItems(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;
const sortedItems = sortItems(allBazaarItems);
displayFullListings(sortedItems, fullListingsView);
displayTopCheapestItems(sortedItems.slice(0, 3), currentItemData ? currentItemData.itemName : "", topCheapestView);
} 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);
}
});
}
function displayFullListings(items, targetElement) {
targetElement.innerHTML = '';
if (items.length === 0) {
targetElement.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';
targetElement.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(createSortableHeader("Price ($) ↑↓", "price"));
headerRow.appendChild(createSortableHeader("Quantity ↑↓", "quantity"));
headerRow.appendChild(createSortableHeader("Updated ↑↓", "last_updated"));
headerRow.appendChild(createCell("Seller"));
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = document.createElement('tbody');
items.forEach((item, index) => {
const tr = document.createElement('tr');
tr.appendChild(createCellWithLink(
`https://www.torn.com/bazaar.php?userID=${item.user_id}`,
`$${item.price.toLocaleString()}`
));
tr.appendChild(createCell(item.quantity));
tr.appendChild(createCell(formatTimestamp(item.last_updated)));
const sellerText = item.player_name ? item.player_name : item.user_id;
tr.appendChild(createCellWithLink(
`https://www.torn.com/profiles.php?XID=${item.user_id}`,
sellerText
));
tbody.appendChild(tr);
});
table.appendChild(tbody);
tableContainer.appendChild(table);
targetElement.appendChild(tableContainer);
adjustUnifiedTableTheme();
}
function displayTopCheapestItems(items, itemName, targetElement) {
targetElement.innerHTML = '';
if (!items || items.length === 0) {
targetElement.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';
targetElement.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(createSortableHeader("Price ($) ↑↓", "price"));
headerRow.appendChild(createSortableHeader("Quantity ↑↓", "quantity"));
headerRow.appendChild(createSortableHeader("Updated ↑↓", "last_updated"));
headerRow.appendChild(createCell("Seller"));
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = document.createElement('tbody');
items.slice(0, 3).forEach((item, index) => {
const tr = document.createElement('tr');
tr.appendChild(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 #ccc';
quantityTd.style.padding = '6px';
quantityTd.style.textAlign = 'center';
quantityTd.style.fontSize = '14px';
tr.appendChild(quantityTd);
const updatedTd = document.createElement('td');
updatedTd.innerText = formatTimestamp(item.last_updated);
updatedTd.style.border = '1px solid #ccc';
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(createCellWithLink(
`https://www.torn.com/profiles.php?XID=${item.user_id}`,
sellerText
));
tbody.appendChild(tr);
});
table.appendChild(tbody);
tableContainer.appendChild(table);
targetElement.appendChild(tableContainer);
adjustUnifiedTableTheme();
}
function adjustUnifiedTableTheme() {
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 = '#f2f2f2';
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';
});
}
});
}
function adjustBazaarEnhancerContainerTheme() {
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)';
}
}
}
function observeDarkMode() {
const observer = new MutationObserver(() => {
adjustUnifiedTableTheme();
adjustBazaarEnhancerContainerTheme();
});
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
}
function injectAdditionalStyles() {
const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#bazaar-enhancer-container {
background-color: #ffffff;
color: #000000;
border: 1px solid #ddd;
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;
}
.dark-mode #bazaar-enhancer-container {
background-color: rgba(0,0,0,0.6);
color: #f0f0f0;
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 4px 8px rgba(0,0,0,0.4);
}
#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 #ccc;
}
#showBazaarModal table.bazaar-table th,
#bazaar-enhancer-container table.top-cheapest-table th {
background-color: #f2f2f2;
}
.dark-mode #showBazaarModal table.bazaar-table th,
.dark-mode #showBazaarModal table.bazaar-table td,
.dark-mode #bazaar-enhancer-container table.top-cheapest-table th,
.dark-mode #bazaar-enhancer-container table.top-cheapest-table td {
background-color: #2a2a2a;
color: #f0f0f0;
}
.dark-mode #showBazaarModal table.bazaar-table th,
.dark-mode #bazaar-enhancer-container table.top-cheapest-table th {
background-color: #333;
}
#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;
margin-bottom: 10px;
}
#bazaar-nav button {
margin: 0 5px;
padding: 5px 10px;
cursor: pointer;
background-color: #f2f2f2;
color: #000;
border: 1px solid #ccc;
}
.dark-mode #bazaar-nav button {
background-color: #444;
color: #fff;
border: 1px solid #666;
}
#topCheapestView, #fullListingsView {
width: 100%;
box-sizing: border-box;
padding: 10px;
}
#fullListingsView {
height: 500px;
overflow-y: auto;
}
`;
document.head.appendChild(style);
}
function checkForItems(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
};
fetchBazaarItems(currentItemData.itemID);
}
}
function checkForItemsMobile() {
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
};
fetchBazaarItems(currentItemData.itemID);
}
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_')) {
checkForItemsMobile();
} else if (window.innerWidth >= 784 && node.className.includes("sellerListWrapper")) {
checkForItems(node);
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(() => {
let info = getItemInfoFromURL();
if (info.itemID) {
currentItemData = info;
fetchBazaarItems(info.itemID);
} else {
if (window.innerWidth < 784) {
checkForItemsMobile();
} else {
const wrapper = document.querySelector('[class*="sellerListWrapper"]');
if (wrapper) checkForItems(wrapper);
else {
clearListingsData();
currentItemData = null;
}
}
}
}, 100);
}
}, 500);
init()
})();