Bazaars in Item Market 2.0

Displays bazaar listings with sorting controls via TornPal & IronNerd

As of 2025-02-22. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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