Bazaar Listings on Market *NO PDA* (TE Market Value, No API Key)

Bazaar listings on Item Page

// ==UserScript==
// @name         Bazaar Listings on Market *NO PDA* (TE Market Value, No API Key)
// @namespace    https://weav3r.dev/
// @version      2.3.4
// @description  Bazaar listings on Item Page
// @author       WTV [3281931]
// @match        https://www.torn.com/*
// @grant        GM_xmlhttpRequest
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    let currentSortKey = 'price';
    let currentSortOrder = 'asc';
    let allListings = [];
    let filteredListings = [];

    window._visitedBazaars = new Set();

    function sortListings(listings) {
        const key = currentSortKey, order = currentSortOrder;
        return [...listings].sort((a,b)=>{
            let valA = a[key] || 0, valB = b[key] || 0;
            if(key==='player_name'){ valA=valA.toLowerCase(); valB=valB.toLowerCase(); }
            return (valA>valB?1:valA<valB?-1:0)*(order==='asc'?1:-1);
        });
    }

    function applyFilters(listings, filters) {
        return listings.filter(l=>{
            if(filters.minPrice && l.price < parseFloat(filters.minPrice)) return false;
            if(filters.maxPrice && l.price > parseFloat(filters.maxPrice)) return false;
            if(filters.minQty && l.quantity < parseInt(filters.minQty)) return false;
            if(filters.maxQty && l.quantity > parseInt(filters.maxQty)) return false;
            return true;
        });
    }

    function renderMessage(container,isError){
        const cardContainer = container.querySelector('.bazaar-card-container');
        if(!cardContainer) return;
        cardContainer.innerHTML = '';
        const msg = document.createElement('div');
        msg.style.cssText='color:#fff;text-align:center;padding:20px;width:100%;';
        msg.innerHTML = isError ? "API Error<br><span style='font-size:12px;color:#ccc;'>Please try again later</span>"
                                : "No bazaar listings available for this item.";
        cardContainer.appendChild(msg);
    }

    function createInfoContainer(itemName,itemId,marketValue){
        const container=document.createElement('div');
        container.className='bazaar-info-container';
        container.dataset.itemid=itemId;
        if(marketValue) container.dataset.marketValue = marketValue;
        container.style.cssText='border:1px solid #888;margin:10px 0;padding:5px;background:#222;color:#fff;';

        const marketText = marketValue ? ` <span style="color:#FFD700;">(Market Value: ${marketValue})</span>` : '';
        container.innerHTML=`
            <div class="bazaar-info-header" style="font-weight:bold;margin-bottom:5px;">
                Bazaar Listings for ${itemName}${marketText}
            </div>
            <div class="bazaar-controls" style="margin-bottom:5px; display:flex; gap:5px; flex-wrap:wrap;"></div>
            <div class="bazaar-card-container" style="display:flex;overflow-x:auto;padding:5px;gap:5px;"></div>
        `;

        const cardContainer = container.querySelector('.bazaar-card-container');
        if(cardContainer){
            cardContainer.addEventListener("wheel", e=>{
                if(e.deltaY!==0){ e.preventDefault(); cardContainer.scrollLeft += e.deltaY; }
            });
        }

        return container;
    }

    function createFilters(container){
        const controls=container.querySelector('.bazaar-controls');
        controls.innerHTML='';

        const sortSelect=document.createElement('select');
        ['price','quantity','player_name'].forEach(opt=>{
            const o=document.createElement('option'); o.value=opt; o.textContent=opt.charAt(0).toUpperCase()+opt.slice(1);
            if(opt===currentSortKey) o.selected=true;
            sortSelect.appendChild(o);
        });
        sortSelect.addEventListener('change', ()=>{ currentSortKey=sortSelect.value; renderCards(container); });

        const orderBtn=document.createElement('button');
        orderBtn.textContent=currentSortOrder==='asc'?'Asc':'Desc';
        orderBtn.addEventListener('click', ()=>{
            currentSortOrder=currentSortOrder==='asc'?'desc':'asc';
            orderBtn.textContent=currentSortOrder==='asc'?'Asc':'Desc';
            renderCards(container);
        });

        const minPrice=document.createElement('input'); minPrice.type='number'; minPrice.placeholder='Min Price'; minPrice.style.width='80px';
        const maxPrice=document.createElement('input'); maxPrice.type='number'; maxPrice.placeholder='Max Price'; maxPrice.style.width='80px';
        const minQty=document.createElement('input'); minQty.type='number'; minQty.placeholder='Min Qty'; minQty.style.width='80px';
        const maxQty=document.createElement('input'); maxQty.type='number'; maxQty.placeholder='Max Qty'; maxQty.style.width='80px';

        const applyBtn=document.createElement('button'); applyBtn.textContent="Apply";
        applyBtn.addEventListener('click', ()=>renderCards(container));

        [minPrice,maxPrice,minQty,maxQty].forEach(inp=>{
            inp.addEventListener('keydown', e=>{ if(e.key==='Enter'){ renderCards(container); } });
        });

        controls.appendChild(sortSelect);
        controls.appendChild(orderBtn);
        [minPrice,maxPrice,minQty,maxQty].forEach(inp=>controls.appendChild(inp));
        controls.appendChild(applyBtn);
    }

    function renderCards(container){
        const cardContainer=container.querySelector('.bazaar-card-container');
        if(!cardContainer || !allListings) return;

        const controls=container.querySelector('.bazaar-controls');
        const filters={
            minPrice: controls.querySelector('input[placeholder="Min Price"]').value,
            maxPrice: controls.querySelector('input[placeholder="Max Price"]').value,
            minQty: controls.querySelector('input[placeholder="Min Qty"]').value,
            maxQty: controls.querySelector('input[placeholder="Max Qty"]').value
        };

        filteredListings=applyFilters(allListings,filters);
        filteredListings=sortListings(filteredListings);

        cardContainer.innerHTML='';
        if(filteredListings.length===0){ renderMessage(container,false); return; }

        const marketValueStr = container.dataset.marketValue;
        const marketNum = marketValueStr ? parseInt(marketValueStr.replace(/\D/g,'')) : null;

        filteredListings.forEach(listing=>{
            const card=document.createElement('div');
            const isVisited=window._visitedBazaars.has(listing.player_id);

            card.style.cssText=`
                border:1px solid #444;
                background:#222;
                color:#eee;
                padding:10px;
                margin:2px;
                min-width:160px;
                cursor:pointer;
                display:flex;
                flex-direction:column;
                font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
                font-size:15px;
                transition:transform 0.2s;
                position: relative;
            `;

            if(!isVisited){
                card.addEventListener('mouseenter', ()=>card.style.transform='scale(1.03)');
                card.addEventListener('mouseleave', ()=>card.style.transform='scale(1)');
            }

            const formattedPrice = `$${Math.round(listing.price).toLocaleString()}`;

            let diffText = '';
            if(marketNum){
                const percent = ((listing.price - marketNum)/marketNum*100).toFixed(1);
                const color = percent < 0 ? 'limegreen' : 'red';
                const sign = percent > 0 ? '+' : '';
                diffText = `<span style="position:absolute; right:10px; top:50%; transform:translateY(-50%); font-weight:bold; color:${color};">${sign}${percent}%</span>`;
            }

            card.innerHTML=`
                <div style="font-weight:bold; color:${isVisited?'#800080':'#1E90FF'}">${listing.player_name || 'Unknown'}</div>
                <div>Qty: ${listing.quantity}</div>
                <div>Price: ${formattedPrice}</div>
                ${diffText}
            `;

            card.addEventListener('click', ()=>{
                if(listing.player_id){
                    window._visitedBazaars.add(listing.player_id);
                    const nameDiv = card.querySelector('div:first-child');
                    if(nameDiv) nameDiv.style.color='#800080';
                    window.open(`https://www.torn.com/bazaar.php?userId=${listing.player_id}&highlightItem=${listing.item_id}#/`,'_blank');
                }
            });

            cardContainer.appendChild(card);
        });
    }

    function updateInfoContainer(wrapper,itemId,itemName){
        let infoContainer=document.querySelector(`.bazaar-info-container[data-itemid="${itemId}"]`);
        if(!infoContainer){
            // Fetch market value from TornExchange public API
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://tornexchange.com/api/te_price?item_id=${itemId}`,
                onload: function(response){
                    let marketValue = '';
                    try{
                        const data = JSON.parse(response.responseText);
                        if(data && data.status === 'success' && data.data && data.data.te_price){
                            const rounded = Math.round(data.data.te_price);
                            marketValue = `$${rounded.toLocaleString()}`;
                        }
                    } catch(e){}
                    // create the info container with marketValue
                    infoContainer = createInfoContainer(itemName, itemId, marketValue);
                    wrapper.insertBefore(infoContainer, wrapper.firstChild);
                    createFilters(infoContainer);
                    fetchBazaarListings(itemId, infoContainer);
                },
                onerror:function(){
                    infoContainer = createInfoContainer(itemName, itemId,'');
                    wrapper.insertBefore(infoContainer, wrapper.firstChild);
                    createFilters(infoContainer);
                    fetchBazaarListings(itemId, infoContainer);
                }
            });
        } else {
            renderCards(infoContainer);
        }
    }

    function fetchBazaarListings(itemId, infoContainer){
        GM_xmlhttpRequest({
            method:"GET",
            url:`https://weav3r.dev/api/marketplace/${itemId}`,
            onload:function(response){
                try{
                    const data = JSON.parse(response.responseText);
                    if(!data || !data.listings){ renderMessage(infoContainer,true); return; }
                    allListings = data.listings.map(l=>({
                        player_name:l.player_name,
                        player_id:l.player_id,
                        quantity:l.quantity,
                        price:l.price,
                        item_id:l.item_id
                    }));
                    renderCards(infoContainer);
                } catch(e){ renderMessage(infoContainer,true); }
            },
            onerror:function(){ renderMessage(infoContainer,true); }
        });
    }

    function processSellerWrapper(wrapper){
        if(!wrapper || wrapper.dataset.bazaarProcessed) return;
        const itemTile = wrapper.closest('[class*="itemTile"]') || wrapper.previousElementSibling;
        if(!itemTile) return;
        let nameEl = itemTile.querySelector('div[class*="name"]') || itemTile.querySelector('div');
        const btn = itemTile.querySelector('button[aria-controls*="itemInfo"]');
        if(!nameEl || !btn) return;
        const itemName = nameEl.textContent.trim();
        const idParts = btn.getAttribute('aria-controls').split('-');
        const itemId = idParts[idParts.length-1];
        wrapper.dataset.bazaarProcessed='true';
        updateInfoContainer(wrapper,itemId,itemName);
    }

    function processAllSellerWrappers(root=document.body){
        const sellerWrappers=root.querySelectorAll('[class*="sellerListWrapper"]');
        sellerWrappers.forEach(wrapper=>processSellerWrapper(wrapper));
    }

    const observer=new MutationObserver(()=>{ processAllSellerWrappers(); });
    observer.observe(document.body,{childList:true,subtree:true});
    processAllSellerWrappers();

})();

// --- Bazaar Page Green Highlight ---
(function(){
    const params=new URLSearchParams(window.location.search);
    const itemIdToHighlight=params.get('highlightItem');
    if(!itemIdToHighlight) return;
    const observer=new MutationObserver(()=>{
        const imgs=document.querySelectorAll('img');
        imgs.forEach(img=>{
            if(img.src.includes(`/images/items/${itemIdToHighlight}/`)){
                img.closest('div')?.style.setProperty('outline','3px solid green','important');
                img.scrollIntoView({behavior:'smooth', block:'center'});
            }
        });
    });
    observer.observe(document.body,{childList:true,subtree:true});
})();