// ==UserScript==
// @name Bazaars in Item Market 2.0
// @namespace http://tampermonkey.net/
// @version 0.5
// @description Displays bazaar listings with sorting controls via TornPal & IronNerd
// @author Weav3r [1853324]
// @match https://www.torn.com/page.php?sid=ItemMarket*
// @grant GM_xmlhttpRequest
// @connect tornpal.com
// @connect www.ironnerd.me
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
const CACHE_DURATION_MS = 60000;
let currentSortKey = "price";
let currentSortOrder = "asc";
function getCache(itemId) {
try {
const key = "tornBazaarCache_" + itemId;
const cached = localStorage.getItem(key);
if (cached) {
const payload = JSON.parse(cached);
if (Date.now() - payload.timestamp < CACHE_DURATION_MS) {
return payload.data;
}
}
} catch(e) {}
return null;
}
function setCache(itemId, data) {
try {
const key = "tornBazaarCache_" + itemId;
const payload = { timestamp: Date.now(), data: data };
localStorage.setItem(key, JSON.stringify(payload));
} catch(e) {}
}
function getRelativeTime(timestampSeconds) {
const now = Date.now();
const diffSec = Math.floor((now - timestampSeconds * 1000) / 1000);
if (diffSec < 60) return diffSec + 's ago';
if (diffSec < 3600) return Math.floor(diffSec / 60) + 'm ago';
if (diffSec < 86400) return Math.floor(diffSec / 3600) + 'h ago';
return Math.floor(diffSec / 86400) + 'd ago';
}
function openModal(url) {
const originalBodyOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
const modalOverlay = document.createElement('div');
modalOverlay.id = 'bazaar-modal-overlay';
Object.assign(modalOverlay.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: '10000',
overflow: 'visible'
});
const modalContainer = document.createElement('div');
modalContainer.id = 'bazaar-modal-container';
Object.assign(modalContainer.style, {
backgroundColor: '#fff',
border: '8px solid #000',
boxShadow: '0 0 10px rgba(0,0,0,0.5)',
borderRadius: '8px',
position: 'relative',
resize: 'both'
});
const savedSize = localStorage.getItem('bazaarModalSize');
if (savedSize) {
try {
const { width, height } = JSON.parse(savedSize);
modalContainer.style.width = (width < 200 ? '80%' : width + 'px');
modalContainer.style.height = (height < 200 ? '80%' : height + 'px');
} catch(e) {
modalContainer.style.width = '80%';
modalContainer.style.height = '80%';
}
} else {
modalContainer.style.width = '80%';
modalContainer.style.height = '80%';
}
const closeButton = document.createElement('button');
closeButton.textContent = '×';
Object.assign(closeButton.style, {
position: 'absolute',
top: '-20px',
right: '-20px',
width: '40px',
height: '40px',
backgroundColor: '#ff0000',
color: '#fff',
border: 'none',
borderRadius: '50%',
fontSize: '24px',
cursor: 'pointer',
boxShadow: '0 0 5px rgba(0,0,0,0.5)'
});
closeButton.addEventListener('click', () => {
modalOverlay.remove();
document.body.style.overflow = originalBodyOverflow;
});
modalOverlay.addEventListener('click', (e) => {
if (e.target === modalOverlay) {
modalOverlay.remove();
document.body.style.overflow = originalBodyOverflow;
}
});
const iframe = document.createElement('iframe');
Object.assign(iframe.style, {
width: '100%',
height: '100%',
border: 'none'
});
iframe.src = url;
modalContainer.appendChild(closeButton);
modalContainer.appendChild(iframe);
modalOverlay.appendChild(modalContainer);
document.body.appendChild(modalOverlay);
if (window.ResizeObserver) {
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
localStorage.setItem('bazaarModalSize', JSON.stringify({
width: Math.round(width),
height: Math.round(height)
}));
}
});
resizeObserver.observe(modalContainer);
}
}
function createInfoContainer(itemName, itemId) {
const container = document.createElement('div');
container.id = 'item-info-container';
container.setAttribute('data-itemid', itemId);
Object.assign(container.style, {
backgroundColor: '#2f2f2f',
color: '#ccc',
fontSize: '13px',
border: '1px solid #444',
borderRadius: '4px',
margin: '5px 0',
padding: '10px',
display: 'flex',
flexDirection: 'column',
gap: '8px'
});
const header = document.createElement('div');
header.className = 'info-header';
Object.assign(header.style, {
fontSize: '16px',
fontWeight: 'bold',
color: '#fff'
});
header.textContent = `Item: ${itemName} (ID: ${itemId})`;
container.appendChild(header);
const sortControls = document.createElement('div');
sortControls.className = 'sort-controls';
Object.assign(sortControls.style, {
display: 'flex',
alignItems: 'center',
gap: '5px',
fontSize: '12px',
padding: '5px',
backgroundColor: '#333',
borderRadius: '4px'
});
const sortLabel = document.createElement('span');
sortLabel.textContent = "Sort by:";
sortControls.appendChild(sortLabel);
const sortSelect = document.createElement('select');
Object.assign(sortSelect.style, {
padding: '2px',
border: '1px solid #444',
borderRadius: '2px',
backgroundColor: '#1a1a1a',
color: '#fff'
});
[
{ value: "price", text: "Price" },
{ value: "quantity", text: "Quantity" },
{ value: "updated", text: "Last Updated" }
].forEach(opt => {
const option = document.createElement('option');
option.value = opt.value;
option.textContent = opt.text;
sortSelect.appendChild(option);
});
sortSelect.value = currentSortKey;
sortControls.appendChild(sortSelect);
const orderToggle = document.createElement('button');
Object.assign(orderToggle.style, {
padding: '2px 4px',
border: '1px solid #444',
borderRadius: '2px',
backgroundColor: '#1a1a1a',
color: '#fff',
cursor: 'pointer'
});
orderToggle.textContent = (currentSortOrder === "asc") ? "Asc" : "Desc";
sortControls.appendChild(orderToggle);
container.appendChild(sortControls);
// Listings row
const scrollWrapper = document.createElement('div');
Object.assign(scrollWrapper.style, {
overflowX: 'auto',
overflowY: 'hidden',
height: '120px',
whiteSpace: 'nowrap',
paddingBottom: '3px'
});
const cardContainer = document.createElement('div');
cardContainer.className = 'card-container';
Object.assign(cardContainer.style, {
display: 'flex',
flexWrap: 'nowrap',
gap: '10px'
});
scrollWrapper.appendChild(cardContainer);
container.appendChild(scrollWrapper);
const poweredBy = document.createElement('div');
poweredBy.style.fontSize = '10px';
poweredBy.style.textAlign = 'right';
poweredBy.style.marginTop = '6px';
poweredBy.innerHTML = `
<span style="color:#666;">Powered by </span>
<a href="https://tornpal.com/" target="_blank" style="color:#aaa; text-decoration:underline;">TornPal</a>
<span style="color:#666;"> & </span>
<a href="https://ironnerd.me/" target="_blank" style="color:#aaa; text-decoration:underline;">IronNerd</a>
`;
container.appendChild(poweredBy);
sortSelect.addEventListener('change', () => {
currentSortKey = sortSelect.value;
if (container.filteredListings) renderCards(container, container.filteredListings);
});
orderToggle.addEventListener('click', () => {
currentSortOrder = (currentSortOrder === "asc") ? "desc" : "asc";
orderToggle.textContent = (currentSortOrder === "asc") ? "Asc" : "Desc";
if (container.filteredListings) renderCards(container, container.filteredListings);
});
return container;
}
function renderCards(infoContainer, listings) {
const sorted = listings.slice().sort((a, b) => {
let diff = 0;
if (currentSortKey === "price") diff = a.price - b.price;
else if (currentSortKey === "quantity") diff = a.quantity - b.quantity;
else if (currentSortKey === "updated") diff = a.updated - b.updated;
return (currentSortOrder === "asc") ? diff : -diff;
});
const cardContainer = infoContainer.querySelector('.card-container');
cardContainer.innerHTML = '';
sorted.forEach(listing => {
const card = createListingCard(listing);
cardContainer.appendChild(card);
});
}
function createListingCard(listing) {
const card = document.createElement('div');
card.className = 'listing-card';
Object.assign(card.style, {
backgroundColor: '#1a1a1a',
color: '#fff',
border: '1px solid #444',
borderRadius: '4px',
padding: '8px',
width: 'calc((100% - 20px) / 3)',
fontSize: 'clamp(12px, 1vw, 16px)',
boxSizing: 'border-box'
});
const linkContainer = document.createElement('div');
Object.assign(linkContainer.style, {
display: 'flex',
alignItems: 'center',
gap: '5px',
marginBottom: '6px'
});
const playerLink = document.createElement('a');
playerLink.href = `https://www.torn.com/bazaar.php?userId=${listing.player_id}#/`;
playerLink.textContent = `Player: ${listing.player_id}`;
Object.assign(playerLink.style, {
fontWeight: 'bold',
color: '#00aaff',
textDecoration: 'underline'
});
linkContainer.appendChild(playerLink);
const iconSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
iconSvg.setAttribute("viewBox", "0 0 512 512");
iconSvg.setAttribute("width", "16");
iconSvg.setAttribute("height", "16");
iconSvg.style.cursor = "pointer";
iconSvg.style.color = "#ffa500";
iconSvg.title = "Open in modal";
iconSvg.innerHTML = `
<path d="M432 64L208 64c-8.8 0-16 7.2-16 16l0 16-64 0 0-16c0-44.2 35.8-80 80-80L432 0c44.2 0 80 35.8 80 80l0 224c0 44.2-35.8 80-80 80l-16 0 0-64 16 0c8.8 0 16-7.2 16-16l0-224c0-8.8-7.2-16-16-16zM0 192c0-35.3 28.7-64 64-64l256 0c35.3 0 64 28.7 64 64l0 256c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 192zm64 32c0 17.7 14.3 32 32 32l192 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L96 192c-17.7 0-32 14.3-32 32z"/>
`;
iconSvg.addEventListener('click', (e) => {
e.preventDefault();
openModal(playerLink.href);
});
linkContainer.appendChild(iconSvg);
const details = document.createElement('div');
details.innerHTML = `
<div><strong>Price:</strong> $${listing.price.toLocaleString()}</div>
<div><strong>Qty:</strong> ${listing.quantity}</div>
`;
details.style.marginBottom = '6px';
const footnote = document.createElement('div');
Object.assign(footnote.style, {
fontSize: '11px',
color: '#aaa',
textAlign: 'right'
});
footnote.textContent = `Updated: ${getRelativeTime(listing.updated)}`;
const sourceInfo = document.createElement('div');
sourceInfo.style.fontSize = '10px';
sourceInfo.style.color = '#aaa';
sourceInfo.style.textAlign = 'right';
let sourceDisplay = (listing.source === "ironnerd") ? "IronNerd" :
(listing.source === "bazaar") ? "TornPal" : listing.source;
sourceInfo.textContent = "Source: " + sourceDisplay;
card.appendChild(linkContainer);
card.appendChild(details);
card.appendChild(footnote);
card.appendChild(sourceInfo);
return card;
}
function updateInfoContainer(wrapper, itemId, itemName) {
let infoContainer = document.querySelector(`#item-info-container[data-itemid="${itemId}"]`);
if (!infoContainer) {
infoContainer = createInfoContainer(itemName, itemId);
wrapper.insertBefore(infoContainer, wrapper.firstChild);
} else {
const header = infoContainer.querySelector('.info-header');
if (header) header.textContent = `Item: ${itemName} (ID: ${itemId})`;
const cardContainer = infoContainer.querySelector('.card-container');
if (cardContainer) cardContainer.innerHTML = '';
}
const cachedData = getCache(itemId);
if (cachedData) {
infoContainer.filteredListings = cachedData.listings;
renderCards(infoContainer, cachedData.listings);
return;
}
let listings = [];
let responsesReceived = 0;
function processResponse(newListings) {
newListings.forEach(newItem => {
let normalized;
if (newItem.user_id !== undefined) {
normalized = {
item_id: newItem.item_id,
player_id: newItem.user_id,
quantity: newItem.quantity,
price: newItem.price,
updated: newItem.last_updated,
source: "ironnerd"
};
} else {
normalized = newItem;
}
let duplicate = listings.find(item =>
item.player_id === normalized.player_id &&
item.price === normalized.price &&
item.quantity === normalized.quantity
);
if (duplicate) {
if (duplicate.source !== normalized.source) {
duplicate.source = "TornPal & IronNerd";
}
} else {
listings.push(normalized);
}
});
responsesReceived++;
if (responsesReceived === 2) {
setCache(itemId, { listings: listings });
infoContainer.filteredListings = listings;
renderCards(infoContainer, listings);
}
}
GM_xmlhttpRequest({
method: 'GET',
url: `https://tornpal.com/api/v1/markets/clist/${itemId}`,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
if (data.listings && Array.isArray(data.listings)) {
const filtered = data.listings.filter(l => l.source === "bazaar");
processResponse(filtered);
} else {
processResponse([]);
}
} catch (e) { processResponse([]); }
},
onerror: function() { processResponse([]); }
});
GM_xmlhttpRequest({
method: 'GET',
url: `https://www.ironnerd.me/get_bazaar_items/${itemId}`,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
if (data.bazaar_items && Array.isArray(data.bazaar_items)) {
processResponse(data.bazaar_items);
} else {
processResponse([]);
}
} catch (e) { processResponse([]); }
},
onerror: function() { processResponse([]); }
});
}
function processSellerWrapper(wrapper) {
if (!wrapper || wrapper.id === 'item-info-container') return;
const itemTile = wrapper.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];
updateInfoContainer(wrapper, itemId, itemName);
}
}
function processMobileSellerList() {
if (window.innerWidth >= 784) return;
const sellerList = document.querySelector('ul.sellerList___e4C9_');
if (!sellerList) {
const existing = document.querySelector('#item-info-container');
if (existing) existing.remove();
return;
}
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 = "unknown";
if (btn) {
const parts = btn.getAttribute('aria-controls').split('-');
itemId = (parts.length > 2) ? parts[parts.length - 2] : parts[parts.length - 1];
}
if (document.querySelector(`#item-info-container[data-itemid="${itemId}"]`)) return;
const infoContainer = createInfoContainer(itemName, itemId);
sellerList.parentNode.insertBefore(infoContainer, sellerList);
updateInfoContainer(infoContainer, itemId, itemName);
}
function processAllSellerWrappers(root = document.body) {
if (window.innerWidth < 784) return;
const wrappers = root.querySelectorAll('[class*="sellerListWrapper"]');
wrappers.forEach(wrapper => processSellerWrapper(wrapper));
}
processAllSellerWrappers();
processMobileSellerList();
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (window.innerWidth < 784) {
if (node.matches('ul.sellerList___e4C9_')) {
processMobileSellerList();
}
} else {
if (node.matches('[class*="sellerListWrapper"]')) {
processSellerWrapper(node);
}
processAllSellerWrappers(node);
}
}
});
mutation.removedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE && node.matches('ul.sellerList___e4C9_')) {
if (window.innerWidth < 784) {
const container = document.querySelector('#item-info-container');
if (container) container.remove();
}
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();