PDA Bazaar Listing on Market(TE Market Value, No API Key) - Formatted Prices

Bazaar listings on Item Page - Works Mobile/PDA

// ==UserScript==
// @name         PDA Bazaar Listing on Market(TE Market Value, No API Key) - Formatted Prices
// @namespace    https://weav3r.dev/
// @version      2.3.5
// @description  Bazaar listings on Item Page - Works Mobile/PDA
// @author       WTV [3281931] - Mobile Compatible Version
// @match        https://www.torn.com/*
// @match        https://m.torn.com/*
// @grant        GM_xmlhttpRequest
// @license MIT
// ==/UserScript==

;(() => {
  let currentSortKey = "price"
  let currentSortOrder = "asc"
  let allListings = []
  const filteredListings = []
  let currentDarkMode = document.body.classList.contains("dark-mode")
  const currentItemName = ""
  const displayMode = "percentage"
  let isMobileView = false

  window._visitedBazaars = new Set()

  const scriptSettings = {
    defaultSort: "price",
    defaultOrder: "asc",
    apiKey: "",
    listingFee: 0,
    defaultDisplayMode: "percentage",
    linkBehavior: "new_tab",
    layoutMode: "default",
  }

  function checkMobileView() {
    isMobileView = window.innerWidth < 784
    return isMobileView
  }
  checkMobileView()
  window.addEventListener("resize", () => {
    checkMobileView()
    processAllSellerWrappers()
  })

  const updateStyles = () => {
    let styleEl = document.getElementById("bazaar-enhanced-styles")

    if (!styleEl) {
      styleEl = document.createElement("style")
      styleEl.id = "bazaar-enhanced-styles"
      document.head.appendChild(styleEl)
    }

    styleEl.textContent = `
      .bazaar-info-container {
        font-size: 15px;
        border-radius: 4px;
        margin: 5px 0;
        padding: 10px;
        display: flex;
        flex-direction: column;
        gap: 8px;
        background-color: #f9f9f9;
        color: #000;
        border: 1px solid #ccc;
        box-sizing: border-box;
        width: 100%;
        overflow: hidden;
      }
      .dark-mode .bazaar-info-container {
        background-color: #2f2f2f;
        color: #ccc;
        border: 1px solid #444;
      }
      .bazaar-info-header {
        font-size: 16px;
        font-weight: bold;
        color: #000;
      }
      .dark-mode .bazaar-info-header {
        color: #fff;
      }
      .bazaar-controls {
        display: flex;
        align-items: center;
        gap: 5px;
        font-size: 12px;
        padding: 8px;
        background-color: #eee;
        border-radius: 4px;
        border: 1px solid #ccc;
        flex-wrap: wrap;
      }
      .dark-mode .bazaar-controls {
        background-color: #333;
        border: 1px solid #444;
      }
      .bazaar-controls select, .bazaar-controls input, .bazaar-controls button {
        padding: 4px 6px;
        border: 1px solid #ccc;
        border-radius: 3px;
        font-size: 11px;
        background: #fff;
        color: #000;
      }
      .dark-mode .bazaar-controls select, 
      .dark-mode .bazaar-controls input, 
      .dark-mode .bazaar-controls button {
        background: #444;
        color: #fff;
        border-color: #666;
      }
      .bazaar-controls input {
        width: 70px;
      }
      .bazaar-card-container {
        display: flex;
        overflow-x: auto;
        gap: 6px;
        padding: 5px;
        min-height: 60px;
      }
      .bazaar-card {
        flex: 0 0 auto;
        min-width: 140px;
        max-width: 140px;
        height: 65px;
        padding: 6px;
        border: 1px solid #ddd;
        border-radius: 4px;
        background: #fff;
        cursor: pointer;
        transition: transform 0.2s;
        position: relative;
        font-size: 15px;
        display: flex;
        flex-direction: column;
        justify-content: space-between;
        overflow: hidden;
      }
      .dark-mode .bazaar-card {
        background: #444;
        border-color: #666;
        color: #fff;
      }
      .bazaar-card:hover {
        transform: scale(1.02);
      }
      .bazaar-percentage {
        position: absolute;
        right: 6px;
        top: 50%;
        transform: translateY(-50%);
        font-weight: bold;
        font-size: 14px;
      }
      @media (max-width: 784px) {
        .bazaar-card {
          min-width: 120px;
          max-width: 120px;
          height: 55px;
          padding: 4px;
          font-size: 14px;
        }
        .bazaar-controls {
          font-size: 10px;
          gap: 3px;
          padding: 6px;
        }
        .bazaar-controls input {
          width: 60px;
        }
        .bazaar-percentage {
          right: 4px;
          font-size: 13px;
        }
      }
    `
  }

  updateStyles()

  const darkModeObserver = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.attributeName === "class") {
        const newDarkMode = document.body.classList.contains("dark-mode")
        if (newDarkMode !== currentDarkMode) {
          currentDarkMode = newDarkMode
          updateStyles()
        }
      }
    })
  })
  darkModeObserver.observe(document.body, { attributes: true })

  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.search && !l.player_name.toLowerCase().includes(filters.search.toLowerCase())) return false
      if (filters.minPrice && l.price < Number.parseFloat(filters.minPrice)) return false
      if (filters.maxPrice && l.price > Number.parseFloat(filters.maxPrice)) return false
      if (filters.minQty && l.quantity < Number.parseInt(filters.minQty)) return false
      if (filters.maxQty && l.quantity > Number.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:#666;text-align:center;padding:20px;"
    msg.innerHTML = isError
      ? "API Error<br><span style='font-size:12px;'>Please try again later</span>"
      : "No bazaar listings available for this item."
    cardContainer.appendChild(msg)
  }

  function createInfoContainer(itemName, itemId) {
    const container = document.createElement("div")
    container.className = "bazaar-info-container"
    container.dataset.itemid = itemId

    container.innerHTML = `
      <div class="bazaar-info-header">Bazaar Listings for ${itemName}</div>
      <div class="market-value-display" style="font-weight: normal; color: #FFD700; font-size: 14px; margin-top: 4px;"></div>
      <div class="bazaar-controls"></div>
      <div class="bazaar-card-container"></div>
    `

    return container
  }

  function createFilters(container) {
    const controls = container.querySelector(".bazaar-controls")
    if (!controls) return

    controls.innerHTML = ""

    const sortSelect = document.createElement("select")
    const sortOptions = [
      { value: "price", text: "Price" },
      { value: "quantity", text: "Quantity" },
      { value: "player_name", text: "Player" },
    ]

    sortOptions.forEach((opt) => {
      const option = document.createElement("option")
      option.value = opt.value
      option.textContent = opt.text
      if (opt.value === currentSortKey) option.selected = true
      sortSelect.appendChild(option)
    })

    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"

    const maxPrice = document.createElement("input")
    maxPrice.type = "number"
    maxPrice.placeholder = "Max Price"

    const minQty = document.createElement("input")
    minQty.type = "number"
    minQty.placeholder = "Min Qty"

    const maxQty = document.createElement("input")
    maxQty.type = "number"
    maxQty.placeholder = "Max Qty"

    const applyBtn = document.createElement("button")
    applyBtn.textContent = "Apply"
    applyBtn.addEventListener("click", () => renderCards(container))
    ;[minPrice, maxPrice, minQty, maxQty].forEach((input) => {
      input.addEventListener("keydown", (e) => {
        if (e.key === "Enter") {
          renderCards(container)
        }
      })
    })

    controls.appendChild(sortSelect)
    controls.appendChild(orderBtn)
    controls.appendChild(minPrice)
    controls.appendChild(maxPrice)
    controls.appendChild(minQty)
    controls.appendChild(maxQty)
    controls.appendChild(applyBtn)
  }

  function processMobileSellerList() {
    if (!checkMobileView()) return

    const existingContainers = document.querySelectorAll(".bazaar-info-container")
    if (existingContainers.length > 0) {
      console.log("[v0] Container already exists globally, skipping")
      return
    }

    const sellerList = document.querySelector('ul.sellerList__e4C9, ul[class*="sellerList"]')
    if (!sellerList) {
      return
    }

    if (sellerList.hasAttribute("data-has-bazaar-container")) {
      return
    }

    const headerEl = document.querySelector(
      '.itemsHeader__ZTO9r .title__ruNCT, [class*="itemsHeader"] [class*="title"]',
    )
    const itemName = headerEl ? headerEl.textContent.trim() : "Unknown"
    const btn = document.querySelector(
      '.itemsHeader___ZTO9r button[aria-controls^="wai-itemInfo-"], [class*="itemsHeader"] 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]
    }

    const infoContainer = createInfoContainer(itemName, itemId)
    sellerList.parentNode.insertBefore(infoContainer, sellerList)
    sellerList.setAttribute("data-has-bazaar-container", "true")

    createFilters(infoContainer)
    fetchMarketValueAndListings(infoContainer, itemId, itemName)
  }

  function fetchMarketValueAndListings(container, itemId, itemName) {
    const marketValuePromise = new Promise((resolve) => {
      window.GM_xmlhttpRequest({
        method: "GET",
        url: `https://tornexchange.com/api/te_price?item_id=${itemId}`,
        onload: (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) {}
          resolve(marketValue)
        },
        onerror: () => resolve(""),
      })
    })

    const listingsPromise = new Promise((resolve) => {
      window.GM_xmlhttpRequest({
        method: "GET",
        url: `https://weav3r.dev/api/marketplace/${itemId}`,
        onload: (response) => {
          try {
            const data = JSON.parse(response.responseText)
            if (!data || !data.listings) {
              resolve(null)
              return
            }
            const listings = 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,
            }))
            resolve(listings)
          } catch (e) {
            resolve(null)
          }
        },
        onerror: () => resolve(null),
      })
    })

    Promise.all([marketValuePromise, listingsPromise]).then(([marketValue, listings]) => {
      if (marketValue) {
        container.dataset.marketValue = marketValue
        const marketValueDisplay = container.querySelector(".market-value-display")
        if (marketValueDisplay) {
          marketValueDisplay.textContent = `Market Value: ${marketValue}`
        }
      }

      if (!listings) {
        renderMessage(container, true)
        return
      }

      allListings = listings
      renderCards(container, marketValue)
    })
  }

  function fetchBazaarListings(itemId, container, marketValue) {
    // This function is no longer needed due to parallel API calls
  }

  function renderCards(container, marketValue) {
    const cardContainer = container.querySelector(".bazaar-card-container")
    if (!cardContainer) return

    const controls = container.querySelector(".bazaar-controls")
    const filters = {}
    if (controls) {
      const inputs = controls.querySelectorAll("input[type='number']")
      inputs.forEach((input) => {
        if (input.placeholder.includes("Min Price")) filters.minPrice = input.value
        if (input.placeholder.includes("Max Price")) filters.maxPrice = input.value
        if (input.placeholder.includes("Min Qty")) filters.minQty = input.value
        if (input.placeholder.includes("Max Qty")) filters.maxQty = input.value
      })
    }

    let processedListings = applyFilters(allListings, filters)
    processedListings = sortListings(processedListings)

    cardContainer.innerHTML = ""

    if (!processedListings || processedListings.length === 0) {
      cardContainer.innerHTML =
        '<div style="color: #666; text-align: center; padding: 20px;">No bazaar listings available</div>'
      return
    }

    const marketNum = marketValue
      ? Number.parseInt(marketValue.replace(/\D/g, ""))
      : container.dataset.marketValue
        ? Number.parseInt(container.dataset.marketValue.replace(/\D/g, ""))
        : null

    processedListings.forEach((listing) => {
      const card = document.createElement("div")
      card.className = "bazaar-card"
      const isVisited = window._visitedBazaars.has(listing.player_id)

      let diffText = ""
      if (marketNum && listing.price) {
        const percent = (((listing.price - marketNum) / marketNum) * 100).toFixed(1)
        const color = percent < 0 ? "limegreen" : "red"
        const sign = percent > 0 ? "+" : ""
        diffText = `<div class="bazaar-percentage" style="color: ${color};">${sign}${percent}%</div>`
      }

      const playerNameHtml = listing.player_name || "Unknown"

      card.innerHTML = `
        <div style="font-weight: bold; color: ${isVisited ? "#800080" : "#1E90FF"}; margin-bottom: 2px; line-height: 1.2; font-size: 16px;">${playerNameHtml}</div>
        <div style="margin-bottom: 1px; font-size: 14px;">Qty: ${listing.quantity}</div>
        <div style="font-size: 14px;">$${Math.round(listing.price).toLocaleString()}</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"
        }
        const bazaarUrl = `https://www.torn.com/bazaar.php?userId=${listing.player_id}&highlightItem=${listing.item_id}#/`
        window.open(bazaarUrl, "_blank")
      })

      cardContainer.appendChild(card)
    })
  }

  function processAllSellerWrappers(root = document.body) {
    const currentUrl = window.location.href
    console.log("[v0] Current URL:", currentUrl)
    console.log("[v0] Is mobile view:", isMobileView)

    const isItemPage =
      currentUrl.includes("&XID=") ||
      currentUrl.includes("/item.php") ||
      currentUrl.includes("items.php?XID=") ||
      (currentUrl.includes("items.php") && currentUrl.includes("XID")) ||
      ((currentUrl.includes("#/market/view=item") ||
        currentUrl.includes("view=item") ||
        (currentUrl.includes("ItemMarket") && currentUrl.includes("itemID="))) &&
        !currentUrl.includes("view=category"))

    console.log("[v0] Is item page:", isItemPage)

    if (!isItemPage) {
      console.log("[v0] Not an item page, skipping")
      return
    }

    const existingContainer = document.querySelector(".bazaar-info-container")
    if (existingContainer) {
      console.log("[v0] Container already exists, skipping")
      return
    }

    console.log("[v0] Processing seller wrappers...")

    if (isMobileView) {
      console.log("[v0] Processing mobile seller list")
      processMobileSellerList()
    }

    const selectors = isMobileView
      ? 'ul.sellerList__e4C9, ul[class*="sellerList"], [class*="seller"], [class*="item"], [class*="wrapper"]'
      : '[class*="sellerListWrapper"]'

    const sellerWrappers = root.querySelectorAll(selectors)
    console.log("[v0] Found seller wrappers:", sellerWrappers.length)
    sellerWrappers.forEach((wrapper) => processSellerWrapper(wrapper))
  }

  function processSellerWrapper(wrapper) {
    if (!wrapper || wrapper.dataset.bazaarProcessed) return

    const currentUrl = window.location.href
    if (!currentUrl.includes("&XID=") && !currentUrl.includes("/item.php")) {
      return
    }

    const existingContainer = document.querySelector(".bazaar-info-container")
    if (existingContainer) {
      return
    }

    let itemTile, nameEl, btn, itemName, itemId

    if (isMobileView) {
      return
    } else {
      itemTile = wrapper.closest('[class*="itemTile"]') || wrapper.previousElementSibling
      if (!itemTile) return

      nameEl = itemTile.querySelector('div[class*="name"]') || itemTile.querySelector("div")
      btn = itemTile.querySelector('button[aria-controls*="itemInfo"]')

      if (!nameEl || !btn) return

      itemName = nameEl.textContent.trim()
      const idParts = btn.getAttribute("aria-controls").split("-")
      itemId = idParts[idParts.length - 1]
    }

    if (!itemId) return

    wrapper.dataset.bazaarProcessed = "true"

    const infoContainer = createInfoContainer(itemName, itemId)
    wrapper.parentNode.insertBefore(infoContainer, wrapper)
    createFilters(infoContainer)
    fetchMarketValueAndListings(infoContainer, itemId, itemName)
  }

  const observeTarget = document.querySelector("#root") || document.body
  let isProcessing = false
  const observer = new MutationObserver((mutations) => {
    if (isProcessing) return
    let needsProcessing = false
    mutations.forEach((mutation) => {
      const isOurMutation = Array.from(mutation.addedNodes).some(
        (node) =>
          node.nodeType === Node.ELEMENT_NODE &&
          (node.classList.contains("bazaar-info-container") || node.querySelector(".bazaar-info-container")),
      )
      if (isOurMutation) return
      mutation.addedNodes.forEach((node) => {
        if (node.nodeType === Node.ELEMENT_NODE) {
          needsProcessing = true
        }
      })
      mutation.removedNodes.forEach((node) => {
        if (
          node.nodeType === Node.ELEMENT_NODE &&
          (node.matches("ul.sellerList__e4C9") || node.matches('ul[class*="sellerList"]')) &&
          checkMobileView()
        ) {
          const container = document.querySelector(".bazaar-info-container")
          if (container) container.remove()
        }
      })
    })
    if (needsProcessing) {
      if (observer.processingTimeout) {
        clearTimeout(observer.processingTimeout)
      }
      observer.processingTimeout = setTimeout(() => {
        try {
          isProcessing = true
          if (checkMobileView()) {
            processMobileSellerList()
          }
        } finally {
          isProcessing = false
          observer.processingTimeout = null
        }
      }, 100)
    }
  })

  observer.observe(observeTarget, {
    childList: true,
    subtree: true,
  })

  if (checkMobileView()) {
    processMobileSellerList()
  }
})()

// --- Bazaar Page Green Highlight (Mobile Compatible) ---
;(() => {
  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 })
})()