// ==UserScript==
// @name Neopets Trading Post Instant Buy Finder (Exact Match + Draggable + Single/Batch)
// @namespace https://scriptneo.com/
// @version 2.3.0
// @description Draggable modal to search Trading Post by exact item name using criteria=item_exact. Single mode searches one item. Batch mode searches many items. Instant Buy lots only, sorted lowest to highest. One request per item, no pagination.
// @author Derek
// @match https://www.neopets.com/island/tradingpost.phtml*
// @grant none
// @run-at document-end
// @downloadURL https://www.scriptneo.com/scripts/download.php?id=24
// @updateURL https://www.scriptneo.com/scripts/download.php?id=24
// ==/UserScript==
(function () {
"use strict";
const API_URL = "https://www.neopets.com/np-templates/ajax/island/tradingpost/tradingpost-list.php";
const DEFAULTS = {
sort: "newest",
minPrice: 1,
maxPrice: 0, // 0 = no max
openLinksNewTab: true,
autoOpenOnTradingPost: true,
autoSearchOnEnter: true,
// Mode
mode: "single", // "single" | "batch"
// Batch tuning
batchDelayMs: 350,
batchMaxItems: 500,
batchReturn: "best", // "best" | "all"
batchSort: "price", // "price" | "name"
// IMPORTANT: exact match API criteria
criteria: "item_exact"
};
let state = {
running: false,
abort: null,
isOpen: false,
drag: { active: false, offsetX: 0, offsetY: 0 }
};
function safeText(s) { return String(s ?? ""); }
function formatNP(n) {
const num = Number(n || 0);
if (!Number.isFinite(num)) return "0";
return num.toLocaleString("en-US");
}
function el(tag, attrs = {}, children = []) {
const node = document.createElement(tag);
for (const [k, v] of Object.entries(attrs)) {
if (k === "class") node.className = v;
else if (k === "style") node.setAttribute("style", v);
else if (k.startsWith("on") && typeof v === "function") node.addEventListener(k.slice(2), v);
else node.setAttribute(k, String(v));
}
for (const child of children) {
if (child == null) continue;
if (typeof child === "string") node.appendChild(document.createTextNode(child));
else node.appendChild(child);
}
return node;
}
function sleep(ms, signal) {
return new Promise((resolve, reject) => {
const t = setTimeout(resolve, ms);
if (signal) {
signal.addEventListener("abort", () => {
clearTimeout(t);
reject(new Error("Aborted"));
}, { once: true });
}
});
}
function stopRun() {
if (state.abort) {
try { state.abort.abort(); } catch (e) {}
}
state.abort = null;
state.running = false;
setBusy(false);
}
function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
function isTradingPostPage() { return /\/island\/tradingpost\.phtml/i.test(location.pathname); }
function setBusy(busy) {
const btn1 = document.getElementById("tp-ibf-search");
const btn2 = document.getElementById("tp-ibf-batch-run");
if (btn1) btn1.disabled = !!busy;
if (btn2) btn2.disabled = !!busy;
}
// ------------------------
// API (Exact match)
// ------------------------
async function tpSearchExactItem(itemName, signal) {
const payload = {
type: "browse",
criteria: DEFAULTS.criteria, // "item_exact"
search_string: itemName,
sort: DEFAULTS.sort,
page: 1
};
const res = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
},
credentials: "include",
body: JSON.stringify(payload),
signal
});
if (!res.ok) throw new Error("HTTP " + res.status + " " + res.statusText);
const data = await res.json();
if (!data || data.success !== true) throw new Error("API returned success=false");
return data;
}
// ------------------------
// Styles + UI
// ------------------------
function injectStylesOnce() {
if (document.getElementById("tp-ibf-style")) return;
const css = `
#tp-ibf-launcher{
position:fixed;right:16px;bottom:16px;z-index:999999;
border:1px solid #1f6feb;background:#1f6feb;color:#fff;border-radius:999px;
padding:10px 14px;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
font-size:13px;font-weight:900;cursor:pointer;box-shadow:0 10px 25px rgba(0,0,0,0.22);
}
#tp-ibf-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:999998;}
#tp-ibf-modal{
position:fixed;left:50%;top:14%;transform:translateX(-50%);
width:560px;max-width:calc(100vw - 32px);max-height:calc(100vh - 32px);
background:#fff;border:1px solid #d0d7de;border-radius:14px;
box-shadow:0 18px 50px rgba(0,0,0,0.26);z-index:999999;overflow:hidden;
font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
}
#tp-ibf-header{
display:flex;align-items:center;justify-content:space-between;
padding:12px;border-bottom:1px solid #eaeef2;background:#f6f8fa;
user-select:none;cursor:move;
}
#tp-ibf-title{display:flex;flex-direction:column;gap:2px;}
#tp-ibf-title .h1{font-weight:950;font-size:14px;color:#111;line-height:1.1;}
#tp-ibf-title .h2{font-size:12px;color:#57606a;}
#tp-ibf-header .btns{display:flex;gap:8px;align-items:center;}
.tp-ibf-btn{
border:1px solid #d0d7de;background:#fff;border-radius:10px;
padding:6px 10px;cursor:pointer;font-size:12px;font-weight:900;
}
.tp-ibf-body{padding:12px;overflow:auto;max-height:calc(100vh - 200px);}
.tp-ibf-row{display:flex;gap:8px;align-items:center;margin-bottom:10px;}
.tp-ibf-input{
flex:1;border:1px solid #d0d7de;border-radius:12px;padding:10px 12px;
font-size:13px;outline:none;
}
.tp-ibf-search{
border:1px solid #1f6feb;background:#1f6feb;color:#fff;border-radius:12px;
padding:10px 12px;font-size:13px;font-weight:950;cursor:pointer;white-space:nowrap;
}
.tp-ibf-search:disabled{opacity:0.6;cursor:not-allowed;}
.tp-ibf-box{border:1px solid #eaeef2;border-radius:12px;padding:10px;margin-bottom:10px;background:#fff;}
.tp-ibf-grid{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;}
.tp-ibf-field{display:flex;flex-direction:column;gap:6px;min-width:0;}
.tp-ibf-label{font-size:11px;color:#57606a;font-weight:900;}
.tp-ibf-num{
width:160px;border:1px solid #d0d7de;border-radius:10px;padding:8px 10px;
font-size:12px;outline:none;
}
.tp-ibf-check{
display:flex;gap:8px;align-items:center;font-size:12px;color:#24292f;
cursor:pointer;user-select:none;
}
.tp-ibf-tabs{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px;}
.tp-ibf-tab{
border:1px solid #d0d7de;background:#fff;border-radius:999px;padding:6px 10px;
cursor:pointer;font-size:12px;font-weight:950;color:#24292f;
}
.tp-ibf-tab.active{border-color:#1f6feb;color:#1f6feb;}
#tp-ibf-status{margin:8px 0 10px 0;font-size:12px;color:#57606a;}
#tp-ibf-results .summary{font-size:12px;color:#24292f;font-weight:950;margin-bottom:10px;}
.tp-ibf-card{border:1px solid #d0d7de;border-radius:14px;padding:12px;background:#fff;margin-bottom:10px;}
.tp-ibf-top{display:flex;justify-content:space-between;gap:10px;align-items:flex-start;}
.tp-ibf-lotid{font-weight:950;font-size:13px;color:#111;word-break:break-word;}
.tp-ibf-meta{font-size:12px;color:#57606a;display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:4px;}
.tp-ibf-badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:900;border:1px solid rgba(0,0,0,0.06);}
.tp-ibf-price{font-weight:950;font-size:14px;color:#1a7f37;text-align:right;}
.tp-ibf-open{
display:inline-block;text-decoration:none;border:1px solid #1f6feb;background:#1f6feb;color:#fff;
border-radius:12px;padding:8px 10px;font-size:12px;font-weight:950;margin-top:6px;float:right;
}
.tp-ibf-items{margin-top:10px;}
.tp-ibf-items .ttl{font-size:11px;color:#57606a;font-weight:950;margin-bottom:6px;}
.tp-ibf-items .list{border:1px solid #eaeef2;border-radius:12px;padding:10px;background:#fff;}
.tp-ibf-items .line{font-size:12px;color:#24292f;line-height:1.35;margin:2px 0;word-break:break-word;}
.tp-ibf-error{border:1px solid #ffebe9;background:#fff1f0;color:#cf222e;border-radius:12px;padding:10px;}
.tp-ibf-error .t1{font-weight:950;margin-bottom:4px;}
.tp-ibf-error .t2{font-size:12px;line-height:1.35;}
.tp-ibf-mini{
font-size:12px;color:#57606a;line-height:1.35;
border:1px solid #eaeef2;background:#f6f8fa;border-radius:12px;padding:10px;margin-bottom:10px;
}
.tp-ibf-smallsel{
border:1px solid #d0d7de;background:#fff;border-radius:10px;padding:8px 10px;
font-size:12px;outline:none;
}
`.trim();
const style = document.createElement("style");
style.id = "tp-ibf-style";
style.textContent = css;
document.head.appendChild(style);
}
function buildLauncher() {
if (document.getElementById("tp-ibf-launcher")) return;
const btn = document.createElement("button");
btn.id = "tp-ibf-launcher";
btn.type = "button";
btn.textContent = "Instant Buy Finder";
btn.addEventListener("click", () => openModal());
document.body.appendChild(btn);
}
function buildModal() {
if (document.getElementById("tp-ibf-modal")) return;
const backdrop = el("div", { id: "tp-ibf-backdrop" });
backdrop.addEventListener("click", () => closeModal());
const modal = el("div", { id: "tp-ibf-modal", role: "dialog", "aria-modal": "true" });
const header = el("div", { id: "tp-ibf-header" }, [
el("div", { id: "tp-ibf-title" }, [
el("div", { class: "h1" }, ["Instant Buy Finder"]),
el("div", { class: "h2" }, ["Exact match only (criteria=item_exact). Instant Buy only. Lowest price first."])
]),
el("div", { class: "btns" }, [
el("button", { class: "tp-ibf-btn", id: "tp-ibf-stop", type: "button" }, ["Stop"]),
el("button", { class: "tp-ibf-btn", id: "tp-ibf-reset", type: "button" }, ["Reset"]),
el("button", { class: "tp-ibf-btn", id: "tp-ibf-close", type: "button" }, ["Close"])
])
]);
const tabs = el("div", { class: "tp-ibf-tabs" }, [
el("button", { id: "tp-ibf-tab-single", class: "tp-ibf-tab", type: "button" }, ["Single mode"]),
el("button", { id: "tp-ibf-tab-batch", class: "tp-ibf-tab", type: "button" }, ["Batch mode"])
]);
const singleHint = el("div", { id: "tp-ibf-hint-single", class: "tp-ibf-mini" }, [
"Single mode: type the exact item name. Example: \"Varia Stamp\" will not match \"Big Bang Varia Stamp\"."
]);
const batchHint = el("div", { id: "tp-ibf-hint-batch", class: "tp-ibf-mini", style: "display:none;" }, [
"Batch mode: one exact item name per line. Returns cheapest lot per item (Best) or all lots (All)."
]);
const singleRow = el("div", { id: "tp-ibf-row-single", class: "tp-ibf-row" }, [
el("input", {
id: "tp-ibf-query",
class: "tp-ibf-input",
type: "text",
placeholder: "Exact item name, for example: Varia Stamp"
}),
el("button", { id: "tp-ibf-search", class: "tp-ibf-search", type: "button" }, ["Search"])
]);
const batchRow = el("div", { id: "tp-ibf-row-batch", style: "display:none;margin-bottom:10px;" }, [
el("textarea", {
id: "tp-ibf-batch",
style: "width:100%;min-height:120px;border:1px solid #d0d7de;border-radius:12px;padding:10px 12px;font-size:13px;outline:none;resize:vertical;"
}, [""]),
el("div", { style: "display:flex;gap:8px;align-items:center;justify-content:space-between;margin-top:8px;flex-wrap:wrap;" }, [
el("div", { style: "display:flex;gap:8px;align-items:center;flex-wrap:wrap;" }, [
el("div", { class: "tp-ibf-field" }, [
el("div", { class: "tp-ibf-label" }, ["Return"]),
el("select", { id: "tp-ibf-batch-return", class: "tp-ibf-smallsel" }, [
el("option", { value: "best" }, ["Best (cheapest per item)"]),
el("option", { value: "all" }, ["All lots (can be long)"])
])
]),
el("div", { class: "tp-ibf-field" }, [
el("div", { class: "tp-ibf-label" }, ["Sort batch by"]),
el("select", { id: "tp-ibf-batch-sort", class: "tp-ibf-smallsel" }, [
el("option", { value: "price" }, ["Price (low to high)"]),
el("option", { value: "name" }, ["Item name (A-Z)"])
])
])
]),
el("button", { id: "tp-ibf-batch-run", class: "tp-ibf-search", type: "button" }, ["Run batch"])
])
]);
const settingsBox = el("div", { class: "tp-ibf-box" }, [
el("div", { class: "tp-ibf-grid" }, [
el("div", { class: "tp-ibf-field" }, [
el("div", { class: "tp-ibf-label" }, ["Min NP"]),
el("input", { id: "tp-ibf-minprice", class: "tp-ibf-num", type: "number", min: "0", value: String(DEFAULTS.minPrice) })
]),
el("div", { class: "tp-ibf-field" }, [
el("div", { class: "tp-ibf-label" }, ["Max NP"]),
el("input", { id: "tp-ibf-maxprice", class: "tp-ibf-num", type: "number", min: "0", value: String(DEFAULTS.maxPrice) })
]),
el("label", { class: "tp-ibf-check", style: "margin-top:18px;" }, [
el("input", { id: "tp-ibf-newtab", type: "checkbox" }),
el("span", {}, ["Open links in new tab"])
])
])
]);
const body = el("div", { class: "tp-ibf-body" }, [
tabs,
singleHint,
batchHint,
singleRow,
batchRow,
settingsBox,
el("div", { id: "tp-ibf-status" }, ["Idle."]),
el("div", { id: "tp-ibf-results" })
]);
modal.appendChild(header);
modal.appendChild(body);
document.body.appendChild(backdrop);
document.body.appendChild(modal);
// Defaults
const newTab = document.getElementById("tp-ibf-newtab");
if (newTab) newTab.checked = DEFAULTS.openLinksNewTab;
const batchReturn = document.getElementById("tp-ibf-batch-return");
if (batchReturn) batchReturn.value = DEFAULTS.batchReturn;
const batchSort = document.getElementById("tp-ibf-batch-sort");
if (batchSort) batchSort.value = DEFAULTS.batchSort;
// Tabs initial
setMode(DEFAULTS.mode);
// Actions
document.getElementById("tp-ibf-close")?.addEventListener("click", () => closeModal());
document.getElementById("tp-ibf-reset")?.addEventListener("click", () => resetUI());
document.getElementById("tp-ibf-stop")?.addEventListener("click", () => {
stopRun();
setStatus("Stopped.", "warn");
});
document.getElementById("tp-ibf-tab-single")?.addEventListener("click", () => setMode("single"));
document.getElementById("tp-ibf-tab-batch")?.addEventListener("click", () => setMode("batch"));
document.getElementById("tp-ibf-search")?.addEventListener("click", () => runSingle());
document.getElementById("tp-ibf-batch-run")?.addEventListener("click", () => runBatch());
document.getElementById("tp-ibf-query")?.addEventListener("keydown", (e) => {
if (!DEFAULTS.autoSearchOnEnter) return;
if (e.key === "Enter") {
e.preventDefault();
runSingle();
}
});
// Draggable
enableDrag(modal, header);
}
function openModal() {
injectStylesOnce();
buildModal();
const backdrop = document.getElementById("tp-ibf-backdrop");
const modal = document.getElementById("tp-ibf-modal");
if (backdrop) backdrop.style.display = "block";
if (modal) modal.style.display = "block";
state.isOpen = true;
setTimeout(() => {
if (getMode() === "single") document.getElementById("tp-ibf-query")?.focus();
else document.getElementById("tp-ibf-batch")?.focus();
}, 50);
}
function closeModal() {
stopRun();
const backdrop = document.getElementById("tp-ibf-backdrop");
const modal = document.getElementById("tp-ibf-modal");
if (backdrop) backdrop.style.display = "none";
if (modal) modal.style.display = "none";
state.isOpen = false;
}
function resetUI() {
const q = document.getElementById("tp-ibf-query");
const ta = document.getElementById("tp-ibf-batch");
const minp = document.getElementById("tp-ibf-minprice");
const maxp = document.getElementById("tp-ibf-maxprice");
const results = document.getElementById("tp-ibf-results");
if (q) q.value = "";
if (ta) ta.value = "";
if (minp) minp.value = String(DEFAULTS.minPrice);
if (maxp) maxp.value = String(DEFAULTS.maxPrice);
if (results) results.innerHTML = "";
setStatus("Idle.", "warn");
}
function setStatus(text, kind) {
const status = document.getElementById("tp-ibf-status");
if (!status) return;
let color = "#57606a";
if (kind === "ok") color = "#1a7f37";
if (kind === "warn") color = "#9a6700";
if (kind === "err") color = "#cf222e";
status.textContent = text;
status.style.color = color;
}
function badge(text, bg, fg) {
return el("span", { class: "tp-ibf-badge", style: "background:" + bg + ";color:" + fg + ";" }, [text]);
}
// ------------------------
// Mode toggle
// ------------------------
function setMode(mode) {
const m = mode === "batch" ? "batch" : "single";
DEFAULTS.mode = m;
const tabSingle = document.getElementById("tp-ibf-tab-single");
const tabBatch = document.getElementById("tp-ibf-tab-batch");
const hintSingle = document.getElementById("tp-ibf-hint-single");
const hintBatch = document.getElementById("tp-ibf-hint-batch");
const rowSingle = document.getElementById("tp-ibf-row-single");
const rowBatch = document.getElementById("tp-ibf-row-batch");
if (tabSingle) tabSingle.classList.toggle("active", m === "single");
if (tabBatch) tabBatch.classList.toggle("active", m === "batch");
if (hintSingle) hintSingle.style.display = m === "single" ? "block" : "none";
if (hintBatch) hintBatch.style.display = m === "batch" ? "block" : "none";
if (rowSingle) rowSingle.style.display = m === "single" ? "flex" : "none";
if (rowBatch) rowBatch.style.display = m === "batch" ? "block" : "none";
const results = document.getElementById("tp-ibf-results");
if (results) results.innerHTML = "";
setStatus("Mode: " + (m === "single" ? "Single" : "Batch") + ".", "warn");
}
function getMode() {
return DEFAULTS.mode === "batch" ? "batch" : "single";
}
// ------------------------
// Dragging
// ------------------------
function enableDrag(modal, handle) {
if (!modal || !handle) return;
const onPointerDown = (e) => {
if (e.button !== 0 && e.pointerType !== "touch") return;
state.drag.active = true;
const rect = modal.getBoundingClientRect();
modal.style.transform = "none";
modal.style.left = rect.left + "px";
modal.style.top = rect.top + "px";
state.drag.offsetX = e.clientX - rect.left;
state.drag.offsetY = e.clientY - rect.top;
try { handle.setPointerCapture(e.pointerId); } catch (err) {}
e.preventDefault();
};
const onPointerMove = (e) => {
if (!state.drag.active) return;
const modalW = modal.offsetWidth || 560;
const modalH = modal.offsetHeight || 320;
const x = clamp(e.clientX - state.drag.offsetX, 8, window.innerWidth - modalW - 8);
const y = clamp(e.clientY - state.drag.offsetY, 8, window.innerHeight - modalH - 8);
modal.style.left = x + "px";
modal.style.top = y + "px";
};
const onPointerUp = (e) => {
if (!state.drag.active) return;
state.drag.active = false;
try { handle.releasePointerCapture(e.pointerId); } catch (err) {}
};
handle.addEventListener("pointerdown", onPointerDown);
window.addEventListener("pointermove", onPointerMove);
window.addEventListener("pointerup", onPointerUp);
}
// ------------------------
// Search logic
// ------------------------
function readPriceFilters() {
const minPrice = Number(document.getElementById("tp-ibf-minprice")?.value ?? DEFAULTS.minPrice) || 0;
const maxPrice = Number(document.getElementById("tp-ibf-maxprice")?.value ?? DEFAULTS.maxPrice) || 0;
return { minPrice, maxPrice };
}
function filterAndSortInstantBuy(lots, minPrice, maxPrice) {
let ibLots = (Array.isArray(lots) ? lots : []).filter(l => Number(l.instant_buy_amount || 0) > 0);
ibLots = ibLots.filter(l => {
const p = Number(l.instant_buy_amount || 0);
if (p < minPrice) return false;
if (maxPrice > 0 && p > maxPrice) return false;
return true;
});
ibLots.sort((a, b) => {
const ap = Number(a.instant_buy_amount || 0);
const bp = Number(b.instant_buy_amount || 0);
if (ap !== bp) return ap - bp;
return Number(b.lot_id || 0) - Number(a.lot_id || 0);
});
return ibLots;
}
function renderNoLots(label) {
const results = document.getElementById("tp-ibf-results");
if (!results) return;
results.innerHTML = "";
results.appendChild(el("div", { class: "tp-ibf-error" }, [
el("div", { class: "t1" }, ["No Instant Buy lots found"]),
el("div", { class: "t2" }, [
"Exact search: \"" + safeText(label) + "\"."
])
]));
}
function renderSingleResults(lots, query) {
const results = document.getElementById("tp-ibf-results");
if (!results) return;
results.innerHTML = "";
if (!lots.length) {
renderNoLots(query);
return;
}
results.appendChild(el("div", { class: "summary" }, [
"Found " + lots.length + " Instant Buy lot" + (lots.length === 1 ? "" : "s") + " (low to high)"
]));
for (const lot of lots) {
results.appendChild(renderLotCard(lot, false));
}
}
function renderBatchResults(rows, label) {
const results = document.getElementById("tp-ibf-results");
if (!results) return;
results.innerHTML = "";
if (!rows.length) {
renderNoLots(label);
return;
}
results.appendChild(el("div", { class: "summary" }, [
"Batch results: " + rows.length + " item" + (rows.length === 1 ? "" : "s")
]));
for (const row of rows) results.appendChild(renderBatchCard(row));
}
function renderBatchCard(row) {
const modeReturn = document.getElementById("tp-ibf-batch-return")?.value || DEFAULTS.batchReturn;
const card = el("div", { class: "tp-ibf-card" }, [
el("div", { class: "tp-ibf-lotid" }, [safeText(row.item)])
]);
if (modeReturn === "best") {
card.appendChild(el("div", { style: "margin-top:8px;" }, [
renderLotCard(row.bestLot, true)
]));
} else {
card.appendChild(el("div", { style: "margin-top:8px;" }, [
el("div", { style: "font-size:12px;color:#57606a;font-weight:900;margin-bottom:6px;" }, [
"Lots (" + row.lots.length + ")"
])
]));
for (const lot of row.lots) card.appendChild(renderLotCard(lot, true));
}
return card;
}
function renderLotCard(lot, compact) {
const openNewTab = document.getElementById("tp-ibf-newtab")?.checked ?? true;
const link = safeText(lot.link || "");
const href = link.startsWith("//")
? ("https:" + link)
: (link.startsWith("http") ? link : ("https://www.neopets.com" + link.replace(/^\/+/, "/")));
const price = Number(lot.instant_buy_amount || 0);
const items = (lot.items || []).map(it => {
const amount = Number(it.amount || 1);
const name = safeText(it.name);
const sub = safeText(it.sub_name);
return (amount > 1 ? (amount + "x ") : "") + name + (sub ? (" " + sub) : "");
});
const owner = safeText(lot.owner);
const isPremium = !!lot.premium_owner;
const outer = el("div", {
class: compact ? "" : "tp-ibf-card",
style: compact ? "border:1px solid #eaeef2;border-radius:12px;padding:10px;margin-top:8px;background:#fff;" : ""
}, [
el("div", { class: "tp-ibf-top" }, [
el("div", {}, [
el("div", { class: "tp-ibf-lotid" }, ["Lot #" + safeText(lot.lot_id)]),
el("div", { class: "tp-ibf-meta" }, [
el("span", {}, ["Owner: " + owner]),
badge(isPremium ? "Premium" : "Standard", isPremium ? "#ddf4ff" : "#f6f8fa", isPremium ? "#0969da" : "#57606a"),
badge("Offers: " + safeText(lot.offer_count ?? 0), "#fff8c5", "#9a6700")
])
]),
el("div", {}, [
el("div", { class: "tp-ibf-price" }, [formatNP(price) + " NP"]),
el("a", {
class: "tp-ibf-open",
href,
target: openNewTab ? "_blank" : "_self",
rel: "noopener noreferrer"
}, ["Open Lot"])
])
]),
el("div", { class: "tp-ibf-items" }, [
el("div", { class: "ttl" }, ["Items"]),
el("div", { class: "list" }, items.length
? items.map(s => el("div", { class: "line" }, [s]))
: [el("div", { class: "line", style: "color:#57606a;" }, ["(No items listed)"])])
])
]);
return outer;
}
async function runSingle() {
const query = (document.getElementById("tp-ibf-query")?.value ?? "").trim();
const { minPrice, maxPrice } = readPriceFilters();
if (!query) {
setStatus("Enter an item name to search.", "err");
renderNoLots("");
return;
}
stopRun();
state.running = true;
state.abort = new AbortController();
setBusy(true);
setStatus("Exact searching \"" + query + "\" ...", "warn");
try {
const data = await tpSearchExactItem(query, state.abort.signal);
const ibLots = filterAndSortInstantBuy(data.lots, minPrice, maxPrice);
if (!ibLots.length) setStatus("No Instant Buy lots found for exact \"" + query + "\".", "err");
else setStatus("Done. Found " + ibLots.length + " Instant Buy lot" + (ibLots.length === 1 ? "" : "s") + ". Sorted low to high.", "ok");
renderSingleResults(ibLots, query);
} catch (e) {
const msg = e && e.message ? e.message : "Request failed";
setStatus("Error: " + msg + ".", "err");
renderNoLots(query);
} finally {
state.running = false;
state.abort = null;
setBusy(false);
}
}
function parseBatchLines(raw) {
const lines = safeText(raw)
.split(/\r?\n/g)
.map(s => s.trim())
.filter(s => s.length > 0);
const seen = new Set();
const out = [];
for (const line of lines) {
const key = line.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
out.push(line);
}
return out;
}
async function runBatch() {
const raw = document.getElementById("tp-ibf-batch")?.value ?? "";
const items = parseBatchLines(raw);
const { minPrice, maxPrice } = readPriceFilters();
const modeReturn = document.getElementById("tp-ibf-batch-return")?.value || DEFAULTS.batchReturn;
const modeSort = document.getElementById("tp-ibf-batch-sort")?.value || DEFAULTS.batchSort;
if (!items.length) {
setStatus("Add item names in Batch mode (one per line).", "err");
renderBatchResults([], "Empty list");
return;
}
if (items.length > DEFAULTS.batchMaxItems) {
setStatus("Too many items. Limit is " + DEFAULTS.batchMaxItems + ".", "err");
renderBatchResults([], "Too many items");
return;
}
stopRun();
state.running = true;
state.abort = new AbortController();
setBusy(true);
const resultsRows = [];
let totalIBLots = 0;
try {
for (let i = 0; i < items.length; i++) {
if (state.abort.signal.aborted) throw new Error("Aborted");
const item = items[i];
setStatus("Batch " + (i + 1) + "/" + items.length + ": exact \"" + item + "\"", "warn");
const data = await tpSearchExactItem(item, state.abort.signal);
const ibLots = filterAndSortInstantBuy(data.lots, minPrice, maxPrice);
totalIBLots += ibLots.length;
if (modeReturn === "best") {
if (ibLots.length) resultsRows.push({ item, bestLot: ibLots[0] });
} else {
if (ibLots.length) resultsRows.push({ item, lots: ibLots });
}
if (i < items.length - 1) await sleep(DEFAULTS.batchDelayMs, state.abort.signal);
}
if (modeSort === "name") {
resultsRows.sort((a, b) => safeText(a.item).localeCompare(safeText(b.item)));
} else {
resultsRows.sort((a, b) => {
const aLot = a.bestLot || (Array.isArray(a.lots) ? a.lots[0] : null);
const bLot = b.bestLot || (Array.isArray(b.lots) ? b.lots[0] : null);
const ap = Number(aLot?.instant_buy_amount || 0);
const bp = Number(bLot?.instant_buy_amount || 0);
if (ap !== bp) return ap - bp;
return safeText(a.item).localeCompare(safeText(b.item));
});
}
if (!resultsRows.length) {
setStatus("Batch done. No Instant Buy lots found (exact match).", "err");
} else {
const label = modeReturn === "best"
? ("Batch done. Best price per item. Items found: " + resultsRows.length + ".")
: ("Batch done. Lots found: " + totalIBLots + " across " + resultsRows.length + " item(s).");
setStatus(label, "ok");
}
renderBatchResults(resultsRows, modeReturn === "best" ? "Best per item" : "All lots");
} catch (e) {
const msg = e && e.message ? e.message : "Request failed";
if (String(msg).toLowerCase().includes("aborted")) setStatus("Stopped.", "warn");
else setStatus("Error: " + msg + ".", "err");
renderBatchResults([], "Error");
} finally {
state.running = false;
state.abort = null;
setBusy(false);
}
}
// ------------------------
// Boot
// ------------------------
function boot() {
injectStylesOnce();
buildLauncher();
if (DEFAULTS.autoOpenOnTradingPost && isTradingPostPage()) openModal();
}
boot();
})();