Bazaar Directory - WTF is in there?

The new bazaar directory feature doesn't tell you anything about what is in each bazaar. This replaces the (largely) useless counter showing how many favorites a bazaar has with a button to show their bazaar's contents instead.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name        Bazaar Directory - WTF is in there?
// @namespace   Violentmonkey Scripts
// @match       https://www.torn.com/page.php*
// @grant       GM_xmlhttpRequest
// @version     1.0.1
// @author      Titanic_
// @license     MIT
// @description The new bazaar directory feature doesn't tell you anything about what is in each bazaar. This replaces the (largely) useless counter showing how many favorites a bazaar has with a button to show their bazaar's contents instead.
// ==/UserScript==

let userApiKey = getData("API_KEY") || "";

const bazaarIconSVG = `<svg class="bazaar-icon" style="scale: 0.75;"></svg>`;
window.MyCustomBazaarInterval = null;

async function fetchApi(endpoint, selections = "basic", apiKeyToUse = userApiKey) {
	if (!apiKeyToUse) {
		console.warn("API Key not set. Cannot fetch API.");
		return Promise.resolve({ error: { code: 0, error: "API Key not set" } });
	}

	return new Promise((resolve) => {
		GM_xmlhttpRequest({
			method: "GET",
			url: `https://api.torn.com/${endpoint}?key=${apiKeyToUse}&selections=${selections}`,
			timeout: 15000,
			onload: function (response) {
				let parsedJson;
				try {
					parsedJson = JSON.parse(response.responseText);
				} catch (e) {
					console.error(`Error parsing JSON response:`, e, "Response:", response.responseText);
					resolve({ error: { error: "JSON Parse Error", details: e.message, responseText: response.responseText } });
					return;
				}

				if (parsedJson?.error) {
					const errorMessage = parsedJson.error.error || JSON.stringify(parsedJson.error);
					if (parsedJson.error.error !== "API Key not set") console.error(`API Error (Status: ${response.status}): ${errorMessage}`);
					resolve(parsedJson);
					return;
				}

				if (response.status >= 200 && response.status < 300) resolve(parsedJson);
				else {
					console.error(`HTTP Error ${response.status}: Non-success status without specific API error in JSON.`, "Response:", response.responseText);
					resolve({
						error: {
							error: `HTTP Error ${response.status}`,
							details: "Server returned non-2xx status without a Torn API error object in JSON.",
							responseText: response.responseText,
						},
					});
				}
			},
			onerror: function (response) {
				console.error("Network Error:", response.statusText || "Unknown network issue", response);
				resolve({ error: { error: "Network Error", details: response.statusText || "Unknown network issue" } });
			},
			ontimeout: function () {
				console.error("Request Timeout");
				resolve({ error: { error: "Request Timeout" } });
			},
		});
	});
}

function checkUrl() {
	if (!window.location.href.includes("page.php?sid=bazaar")) {
		if (window.MyCustomBazaarInterval) {
			clearInterval(window.MyCustomBazaarInterval);
			window.MyCustomBazaarInterval = null;
		}
		return;
	}

	addBazaarIcons();
}

function addBazaarIcons() {
	document.querySelectorAll("li[class^=bazaarWrap]").forEach((row) => {
		const linkEl = row.querySelector("a[href*='bazaar.php']");
		if (!linkEl) return;

		const statsWrap = linkEl.querySelector("div[class^=statsWrap]");
		if (statsWrap) statsWrap.remove();

		if (!linkEl.querySelector(".bazaar-icon-container")) {
			const bazaarIcon = Object.assign(document.createElement("div"), {
				className: "bazaar-icon-container",
				innerHTML: bazaarIconSVG,
				style: "cursor: pointer; float: right; padding-left: 8px;",
			});

			linkEl.append(bazaarIcon);

			if (!bazaarIcon.dataset.listenerAttached) {
				bazaarIcon.addEventListener("click", (e) => {
					e.preventDefault();
					e.stopPropagation();
					toggleExpand(row);
				});
				bazaarIcon.dataset.listenerAttached = "true";
			}
		}
	});
}

function toggleExpand(row) {
	const existingDetailsDiv = row.querySelector(".expanded-bazaar-details");

	document.querySelectorAll(".expanded-bazaar-details").forEach((div) => {
		if (div.parentElement !== row) div.style.display = "none";
	});

	if (existingDetailsDiv) existingDetailsDiv.style.display = existingDetailsDiv.style.display === "none" ? "block" : "none";
	else {
		const detailsDiv = Object.assign(document.createElement("div"), {
			className: "expanded-bazaar-details",
			style: "",
		});

		const filterInput = Object.assign(document.createElement("input"), {
			type: "text",
			placeholder: "Filter item name",
			style: "width: calc(100% + 10px); text-align: center; background-color: #333333; color: #e0e0e0 !important; border: 1px outset #4f4f4f; padding: 3px;",
		});
		filterInput.addEventListener("input", () => {
			filterTable(table, filterInput.value);
		});
		detailsDiv.appendChild(filterInput);

		const table = Object.assign(document.createElement("table"), {
			style: "width: 100%; border-collapse: collapse; background-color: #383838;",
		});

		const headerRow = table.createTHead().insertRow();
		const columnHeaders = ["Name", "#", "$"];
		columnHeaders.forEach((header) => {
			headerRow.appendChild(
				Object.assign(document.createElement("th"), {
					textContent: header,
					style: `border: 1px solid #4F4F4F; padding: 5px; text-align: ${header == "Name" ? "left" : "right"}; background-color: #454545; color: #e0e0e0;`,
				})
			);
		});

		const placeholderCell = table.createTBody().insertRow().insertCell();
		Object.assign(placeholderCell, {
			colSpan: columnHeaders.length,
			textContent: "Details will be loaded here.",
			style: "text-align: center; padding: 3px; font-style: italic; color: #a0a0a0; border: 1px solid #4F4F4F;",
		});

		detailsDiv.appendChild(table);
		row.appendChild(detailsDiv);
		detailsDiv.style.display = "block";

		populateBazaar(row, table);
	}
}

function filterTable(table, searchText) {
	Array.from(table.querySelector("tbody").querySelectorAll("tr")).forEach((row) => {
		const nameCell = row.querySelector("td:first-child");
		if (nameCell) {
			const name = nameCell.textContent.toLowerCase();
			if (name.includes(searchText.toLowerCase())) row.style.display = "";
			else row.style.display = "none";
		}
	});
}

async function populateBazaar(row, table) {
	const url = row.querySelector("a[href*='bazaar.php']").href;
	const userID = new URL(url).searchParams.get("userId");
	const data = await fetchApi(`user/${userID}`, "bazaar");

	const tbody = table.querySelector("tbody");
	tbody.innerHTML = "";

	if (data.error) {
		const errorRow = tbody.insertRow();
		const errorCell = errorRow.insertCell();
		errorCell.colSpan = 3;
		errorCell.style = "text-align: center; padding: 10px; font-style: italic; color: #a0a0a0; border: 1px solid #4F4F4F;";

		if (data.error.error === "API Key not set") {
			const setKeyLink = Object.assign(document.createElement("a"), {
				href: "#",
				textContent: "Click to set Public API",
				style: "color: #88C9F2; cursor: pointer;",
			});

			setKeyLink.onclick = async (e) => {
				e.preventDefault();
				const newApiKeyInput = prompt("Please enter your Torn API (Public) key:");
				if (newApiKeyInput) {
					const trimmedKey = newApiKeyInput.trim();
					if (trimmedKey !== "") {
						setData("API_KEY", trimmedKey);
						userApiKey = trimmedKey;

						tbody.innerHTML = "";
						const loadingRow = tbody.insertRow();
						const loadingCell = loadingRow.insertCell();
						loadingCell.colSpan = 3;
						loadingCell.textContent = "Reloading bazaar data...";
						loadingCell.style = "text-align: center; padding: 10px; font-style: italic; color: #a0a0a0; border: 1px solid #4F4F4F;";

						await populateBazaar(row, table);
					} else {
						alert("API Key cannot be empty.");
					}
				}
			};
			errorCell.innerHTML = "";
			errorCell.appendChild(setKeyLink);
		} else errorCell.textContent = `Error loading bazaar: ${data.error.error}`;
		return;
	}

	if (!data.bazaar || data.bazaar.length === 0) {
		const noItemsRow = tbody.insertRow();
		const noItemsCell = noItemsRow.insertCell();
		noItemsCell.colSpan = 3;
		noItemsCell.textContent = "No items available in this bazaar.";
		noItemsCell.style = "text-align: center; padding: 10px; font-style: italic; color: #a0a0a0; border: 1px solid #4F4F4F;";
		return;
	}

	const items = data.bazaar.map((item) => ({
		name: item.name,
		amount: item.quantity,
		price: item.price,
	}));

	const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));

	for (const item of sortedItems) {
		const itemRow = tbody.insertRow();

		itemRow.appendChild(
			Object.assign(document.createElement("td"), {
				textContent: item.name,
				style:
					"border: 1px solid #4F4F4F; padding: 5px; color: #E0E0E0 !important; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100px;",
			})
		);

		itemRow.appendChild(
			Object.assign(document.createElement("td"), {
				textContent: item.amount.toLocaleString(),
				style: "border: 1px solid #4F4F4F; padding: 5px; color: #E0E0E0 !important; text-align: right;",
			})
		);

		itemRow.appendChild(
			Object.assign(document.createElement("td"), {
				textContent: "$" + item.price.toLocaleString(),
				style: "border: 1px solid #4F4F4F; padding: 5px; color: #E0E0E0 !important; text-align: right;",
			})
		);
	}

	const filterInput = row.querySelector('.expanded-bazaar-details > input[type="text"]');
	if (filterInput) {
		filterTable(table, filterInput.value);
	}
}

function getData(key) {
	return localStorage.getItem(key);
}

function setData(key, value) {
	localStorage.setItem(key, value);
}

function addStyle(css) {
	const styleEl = Object.assign(document.createElement("style"), { type: "text/css" });
	styleEl.appendChild(document.createTextNode(css));
	document.head.appendChild(styleEl);
}

if (window.MyCustomBazaarInterval) clearInterval(window.MyCustomBazaarInterval);
window.MyCustomBazaarInterval = setInterval(checkUrl, 1000);
checkUrl();

addStyle(`
    .bazaarWrap___XXYgz {
        flex-direction: column;
        height: fit-content !important;
    }
    .expanded-bazaar-details {
        display: block;
        max-width: 100%;
        width: 100%;
        max-height: 200px;
        overflow-y: scroll;
        overflow-x: hidden;
        background-color: #383838;
        border-top: 1px solid #222222;
        clear: both; color: #cccccc;
        padding-right: 10px;
    }
`);