Trading Post By web crawler 1 installs Rating 0.0 (0) approved

Neopets - Trading Post Bulk Delete Helper

Adds a panel to select, delete selected, or delete all of your visible Trading Post lots.
bulk delete neopets trading post
Install
https://www.scriptneo.com/script/neopets-trading-post-bulk-delete-helper

Version selector


SHA256
fb4c39d59a6cb4f1ac8c251eccb64b1825fcd9fac91351425acce8d4122c18ed
No scan flags on this version.

Source code

// ==UserScript==
// @name         Neopets - Trading Post Bulk Delete Helper
// @version      1.1
// @description  Adds a panel to select, delete selected, or delete all of your visible Trading Post lots.
// @author       Fixed Version
// @match        *://www.neopets.com/island/tradingpost.phtml*
// @match        *://neopets.com/island/tradingpost.phtml*
// @grant        none
// @icon         https://images.neopets.com/tradingpost/assets/images/trade_icon.png
// @downloadURL  https://www.scriptneo.com/scripts/download.php?id=28
// @updateURL    https://www.scriptneo.com/scripts/download.php?id=28
// ==/UserScript==

(function () {
    "use strict";

    const TPBH = {
        panelId: "tpbh-panel",
        statusId: "tpbh-status",
        listId: "tpbh-list",

        endpointLots: "/np-templates/ajax/island/tradingpost/get-lots.php?sort=newest",
        endpointDelete: "/np-templates/ajax/island/tradingpost/delete-lot.php",

        delayBetweenDeletesMs: 900,
        requireTypedConfirm: true
    };

    let lots = [];
    let selectedLotIds = new Set();
    let isDeleting = false;

    ready(function () {
        addCSS();
        addPanel();
        loadLots();
    });

    function ready(fn) {
        if (document.readyState === "loading") {
            document.addEventListener("DOMContentLoaded", fn);
        } else {
            fn();
        }
    }

    function qs(selector, root = document) {
        return root.querySelector(selector);
    }

    function qsa(selector, root = document) {
        return Array.from(root.querySelectorAll(selector));
    }

    function sleep(ms) {
        return new Promise(function (resolve) {
            window.setTimeout(resolve, ms);
        });
    }

    function setStatus(message, type = "") {
        const status = qs("#" + TPBH.statusId);

        if (!status) {
            return;
        }

        status.textContent = message;
        status.className = "tpbh-status";

        if (type) {
            status.classList.add(type);
        }
    }

    function addPanel() {
        if (qs("#" + TPBH.panelId)) {
            return;
        }

        const panel = document.createElement("div");
        panel.id = TPBH.panelId;
        panel.innerHTML = `
            <div class="tpbh-header">
                <div>
                    <div class="tpbh-title">Trading Post Bulk Delete</div>
                    <div class="tpbh-subtitle">Select trades, then delete/cancel them in a controlled batch.</div>
                </div>
                <button type="button" id="tpbh-refresh" class="tpbh-btn tpbh-btn-neutral">Refresh</button>
            </div>

            <div class="tpbh-warning">
                Deleting/cancelling lots costs NP. Your example response shows <strong>15,000 NP</strong> for one deleted lot.
            </div>

            <div class="tpbh-actions">
                <button type="button" id="tpbh-select-all" class="tpbh-btn tpbh-btn-neutral">Select All Visible</button>
                <button type="button" id="tpbh-clear" class="tpbh-btn tpbh-btn-neutral">Clear Selection</button>
                <button type="button" id="tpbh-delete-selected" class="tpbh-btn tpbh-btn-danger">Delete Selected</button>
                <button type="button" id="tpbh-delete-all" class="tpbh-btn tpbh-btn-dark">Delete All Loaded</button>
            </div>

            <div id="${TPBH.statusId}" class="tpbh-status">Loading your trades...</div>
            <div id="${TPBH.listId}" class="tpbh-list"></div>
        `;

        const target =
            qs(".tp-main-content") ||
            qs("#tp_app__2025") ||
            qs("#content") ||
            qs("#main") ||
            document.body;

        target.insertBefore(panel, target.firstChild);

        qs("#tpbh-refresh").addEventListener("click", function () {
            if (!isDeleting) {
                loadLots();
            }
        });

        qs("#tpbh-select-all").addEventListener("click", function () {
            lots.forEach(function (lot) {
                selectedLotIds.add(String(getLotId(lot)));
            });

            renderLots();
            updateSelectionStatus();
        });

        qs("#tpbh-clear").addEventListener("click", function () {
            selectedLotIds.clear();
            renderLots();
            updateSelectionStatus();
        });

        qs("#tpbh-delete-selected").addEventListener("click", function () {
            deleteSelectedLots();
        });

        qs("#tpbh-delete-all").addEventListener("click", function () {
            deleteAllLoadedLots();
        });
    }

    async function loadLots() {
        try {
            selectedLotIds.clear();
            setStatus("Loading your Trading Post lots...", "tpbh-working");

            const response = await fetch(TPBH.endpointLots, {
                method: "GET",
                credentials: "include",
                headers: {
                    "x-requested-with": "XMLHttpRequest"
                },
                cache: "no-store"
            });

            if (!response.ok) {
                throw new Error("Could not load trades. HTTP " + response.status);
            }

            const data = await response.json();

            console.log("[TPBH] get-lots response:", data);

            const rawLots = normalizeLotsResponse(data);

            lots = rawLots.filter(function (lot) {
                return getLotId(lot);
            });

            renderLots();

            if (lots.length === 0) {
                setStatus("No active trades found.", "tpbh-success");
            } else {
                setStatus("Loaded " + lots.length + " trade(s).", "tpbh-success");
            }
        } catch (error) {
            console.error("[TPBH] loadLots error:", error);
            setStatus("ERROR: " + error.message, "tpbh-error");
        }
    }

    function normalizeLotsResponse(data) {
        if (!data) {
            return [];
        }

        if (Array.isArray(data)) {
            return data;
        }

        if (Array.isArray(data.lots)) {
            return data.lots;
        }

        if (Array.isArray(data.trades)) {
            return data.trades;
        }

        if (Array.isArray(data.results)) {
            return data.results;
        }

        if (data.data && Array.isArray(data.data.lots)) {
            return data.data.lots;
        }

        if (data.data && Array.isArray(data.data.trades)) {
            return data.data.trades;
        }

        return [];
    }

    function renderLots() {
        const list = qs("#" + TPBH.listId);

        if (!list) {
            return;
        }

        if (!lots.length) {
            list.innerHTML = `<div class="tpbh-empty">No active trades found.</div>`;
            return;
        }

        list.innerHTML = lots.map(function (lot) {
            const lotId = String(getLotId(lot));
            const selected = selectedLotIds.has(lotId);
            const title = getLotTitle(lot);
            const wish = getLotWishlist(lot);
            const itemCount = getLotItemCount(lot);
            const offerCount = getLotOfferCount(lot);
            const instantBuy = getLotInstantBuy(lot);

            return `
                <label class="tpbh-lot ${selected ? "tpbh-lot-selected" : ""}" data-lot-id="${escapeHtml(lotId)}">
                    <input type="checkbox" class="tpbh-check" value="${escapeHtml(lotId)}" ${selected ? "checked" : ""}>
                    <div class="tpbh-lot-body">
                        <div class="tpbh-lot-top">
                            <strong>Lot ${escapeHtml(lotId)}</strong>
                            <span>${escapeHtml(itemCount)} item(s)</span>
                        </div>
                        <div class="tpbh-lot-title">${escapeHtml(title)}</div>
                        ${wish ? `<div class="tpbh-lot-wish">Wishlist: ${escapeHtml(wish)}</div>` : ""}
                        <div class="tpbh-lot-meta">
                            <span>Offers: ${escapeHtml(offerCount)}</span>
                            ${instantBuy ? `<span>Instant Buy: ${escapeHtml(instantBuy)} NP</span>` : ""}
                        </div>
                    </div>
                </label>
            `;
        }).join("");

        qsa(".tpbh-check", list).forEach(function (checkbox) {
            checkbox.addEventListener("change", function () {
                const lotId = String(checkbox.value);

                if (checkbox.checked) {
                    selectedLotIds.add(lotId);
                } else {
                    selectedLotIds.delete(lotId);
                }

                renderLots();
                updateSelectionStatus();
            });
        });
    }

    function updateSelectionStatus() {
        if (!lots.length) {
            setStatus("No active trades found.", "tpbh-success");
            return;
        }

        setStatus(
            selectedLotIds.size + " selected out of " + lots.length + " loaded trade(s).",
            "tpbh-working"
        );
    }

    async function deleteSelectedLots() {
        const selected = Array.from(selectedLotIds);

        if (!selected.length) {
            alert("Please select at least one trade first.");
            return;
        }

        await confirmAndDelete(selected, "selected");
    }

    async function deleteAllLoadedLots() {
        const all = lots.map(function (lot) {
            return String(getLotId(lot));
        }).filter(Boolean);

        if (!all.length) {
            alert("No trades are loaded.");
            return;
        }

        await confirmAndDelete(all, "all loaded");
    }

    async function confirmAndDelete(lotIds, label) {
        if (isDeleting) {
            return;
        }

        const estimatedFee = lotIds.length * 15000;

        const message =
            "You are about to delete/cancel " + lotIds.length + " " + label + " trade(s).\n\n" +
            "Estimated fee using your latest example: " + formatNumber(estimatedFee) + " NP\n\n" +
            "Items should be returned to your inventory, but this action cannot be undone from the script.\n\n" +
            (
                TPBH.requireTypedConfirm
                    ? "Type DELETE to continue."
                    : "Press OK to continue."
            );

        if (TPBH.requireTypedConfirm) {
            const typed = window.prompt(message, "");

            if (typed !== "DELETE") {
                setStatus("Cancelled. Nothing was deleted.", "tpbh-success");
                return;
            }
        } else if (!window.confirm(message)) {
            setStatus("Cancelled. Nothing was deleted.", "tpbh-success");
            return;
        }

        await deleteLotsSequentially(lotIds);
    }

    async function deleteLotsSequentially(lotIds) {
        isDeleting = true;
        toggleButtons(true);

        let successCount = 0;
        let failCount = 0;
        let totalCost = 0;
        let totalItemsReturned = 0;
        let totalOffersDeleted = 0;
        const failed = [];

        try {
            for (let i = 0; i < lotIds.length; i++) {
                const lotId = String(lotIds[i]);

                setStatus(
                    "Deleting trade " + (i + 1) + " / " + lotIds.length + " | Lot " + lotId + "...",
                    "tpbh-working"
                );

                try {
                    const result = await deleteSingleLot(lotId);

                    console.log("[TPBH] delete result for lot " + lotId + ":", result);

                    if (result && result.success === true) {
                        successCount++;

                        totalCost += Number(result.deletion_cost || 0);
                        totalItemsReturned += Number(result.items_returned || 0);
                        totalOffersDeleted += Number(result.offers_deleted || 0);

                        selectedLotIds.delete(lotId);

                        lots = lots.filter(function (lot) {
                            return String(getLotId(lot)) !== lotId;
                        });

                        renderLots();

                        setStatus(
                            "Deleted " + successCount + " / " + lotIds.length +
                            " | Cost: " + formatNumber(totalCost) + " NP" +
                            " | Items returned: " + formatNumber(totalItemsReturned),
                            "tpbh-working"
                        );
                    } else {
                        failCount++;
                        failed.push({
                            lotId: lotId,
                            error: result && (result.error || result.message)
                                ? result.error || result.message
                                : "Unknown error"
                        });
                    }
                } catch (error) {
                    failCount++;
                    failed.push({
                        lotId: lotId,
                        error: error.message
                    });
                }

                await sleep(TPBH.delayBetweenDeletesMs);
            }

            if (failCount > 0) {
                console.warn("[TPBH] Failed deletes:", failed);

                setStatus(
                    "Done. Deleted " + successCount +
                    ". Failed " + failCount +
                    ". Cost: " + formatNumber(totalCost) + " NP. Check console.",
                    "tpbh-error"
                );
            } else {
                setStatus(
                    "Done. Deleted " + successCount +
                    " trade(s). Cost: " + formatNumber(totalCost) +
                    " NP. Items returned: " + formatNumber(totalItemsReturned) +
                    ". Offers deleted: " + formatNumber(totalOffersDeleted) + ".",
                    "tpbh-success"
                );
            }
        } finally {
            isDeleting = false;
            toggleButtons(false);

            window.setTimeout(function () {
                loadLots();
            }, 900);
        }
    }

    async function deleteSingleLot(lotId) {
        const numericLotId = Number(lotId);

        if (!Number.isInteger(numericLotId) || numericLotId <= 0) {
            throw new Error("Invalid lot ID: " + lotId);
        }

        const response = await fetch(TPBH.endpointDelete, {
            method: "POST",
            credentials: "include",
            headers: {
                "Content-Type": "application/json",
                "x-requested-with": "XMLHttpRequest"
            },
            body: JSON.stringify({
                lot_id: numericLotId
            })
        });

        const text = await response.text();

        if (!response.ok) {
            throw new Error("HTTP " + response.status + ": " + text.slice(0, 200));
        }

        try {
            return JSON.parse(text);
        } catch (error) {
            console.error("[TPBH] Non-JSON delete response:", text);

            return {
                success: false,
                message: "Non-JSON response from delete-lot.php"
            };
        }
    }

    function toggleButtons(disabled) {
        qsa("#" + TPBH.panelId + " button").forEach(function (button) {
            button.disabled = disabled;
        });
    }

    function getLotId(lot) {
        return (
            lot.lot_id ||
            lot.lotId ||
            lot.id ||
            lot.trade_id ||
            lot.tradeId ||
            ""
        );
    }

    function getLotTitle(lot) {
        if (lot.title) {
            return lot.title;
        }

        if (lot.name) {
            return lot.name;
        }

        if (lot.item_name) {
            return lot.item_name;
        }

        if (Array.isArray(lot.lot_items) && lot.lot_items.length) {
            return lot.lot_items.map(function (item) {
                return item.name || item.item_name || item.itemName || "Item";
            }).slice(0, 3).join(", ");
        }

        if (Array.isArray(lot.items) && lot.items.length) {
            return lot.items.map(function (item) {
                return item.name || item.item_name || item.itemName || "Item";
            }).slice(0, 3).join(", ");
        }

        return "Trading Post Lot";
    }

    function getLotWishlist(lot) {
        return (
            lot.wishlist ||
            lot.wish_list ||
            lot.description ||
            lot.lot_wishlist ||
            lot.lotWishlist ||
            ""
        );
    }

    function getLotItemCount(lot) {
        if (typeof lot.item_count !== "undefined") {
            return lot.item_count;
        }

        if (typeof lot.itemCount !== "undefined") {
            return lot.itemCount;
        }

        if (typeof lot.count !== "undefined") {
            return lot.count;
        }

        if (Array.isArray(lot.lot_items)) {
            return lot.lot_items.length;
        }

        if (Array.isArray(lot.items)) {
            return lot.items.length;
        }

        return "?";
    }

    function getLotOfferCount(lot) {
        if (typeof lot.offer_count !== "undefined") {
            return lot.offer_count;
        }

        if (typeof lot.offers_count !== "undefined") {
            return lot.offers_count;
        }

        if (typeof lot.offerCount !== "undefined") {
            return lot.offerCount;
        }

        if (typeof lot.offers !== "undefined") {
            if (Array.isArray(lot.offers)) {
                return lot.offers.length;
            }

            return lot.offers;
        }

        return "0";
    }

    function getLotInstantBuy(lot) {
        const value =
            lot.instant_buy_amount ||
            lot.instantBuyAmount ||
            lot.instant_buy ||
            lot.instantBuy ||
            "";

        if (!value) {
            return "";
        }

        return formatNumber(value);
    }

    function formatNumber(value) {
        const number = Number(String(value).replace(/,/g, ""));

        if (!Number.isFinite(number)) {
            return String(value);
        }

        return new Intl.NumberFormat("en-US").format(number);
    }

    function escapeHtml(value) {
        return String(value)
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#039;");
    }

    function addCSS() {
        if (qs("#tpbh-style")) {
            return;
        }

        const style = document.createElement("style");
        style.id = "tpbh-style";
        style.textContent = `
            #tpbh-panel {
                background: #fff;
                border: 3px solid #6d4b9b;
                border-radius: 12px;
                margin: 16px auto;
                padding: 14px;
                max-width: 980px;
                color: #111;
                box-shadow: 0 3px 14px rgba(0, 0, 0, 0.18);
                font-family: Arial, sans-serif;
                box-sizing: border-box;
            }

            .tpbh-header {
                display: flex;
                align-items: center;
                justify-content: space-between;
                gap: 12px;
                margin-bottom: 10px;
            }

            .tpbh-title {
                font-size: 20px;
                font-weight: 800;
                color: #4f2f78;
            }

            .tpbh-subtitle {
                font-size: 13px;
                color: #444;
                margin-top: 2px;
            }

            .tpbh-warning {
                background: #fff3cd;
                border: 1px solid #e7c76b;
                color: #4d3900;
                padding: 10px;
                border-radius: 8px;
                margin: 10px 0;
                font-size: 13px;
            }

            .tpbh-actions {
                display: flex;
                flex-wrap: wrap;
                gap: 8px;
                margin: 12px 0;
            }

            .tpbh-btn {
                border: 0;
                border-radius: 8px;
                padding: 9px 12px;
                font-weight: 800;
                cursor: pointer;
                font-size: 13px;
            }

            .tpbh-btn:disabled {
                opacity: 0.55;
                cursor: default;
            }

            .tpbh-btn-neutral {
                background: #e9e4f2;
                color: #2c1d3d;
            }

            .tpbh-btn-danger {
                background: #b71c1c;
                color: #fff;
            }

            .tpbh-btn-dark {
                background: #2b1d3a;
                color: #fff;
            }

            .tpbh-status {
                margin: 10px 0;
                padding: 9px 10px;
                border-radius: 8px;
                background: #eef1f5;
                color: #222;
                font-size: 13px;
                font-weight: 700;
            }

            .tpbh-status.tpbh-working {
                background: #e6f0ff;
                color: #0f3d75;
            }

            .tpbh-status.tpbh-success {
                background: #dff3e3;
                color: #145c25;
            }

            .tpbh-status.tpbh-error {
                background: #f8d7da;
                color: #842029;
            }

            .tpbh-list {
                display: grid;
                grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
                gap: 10px;
                margin-top: 10px;
            }

            .tpbh-lot {
                display: flex;
                gap: 10px;
                align-items: flex-start;
                border: 2px solid #ddd;
                border-radius: 10px;
                padding: 10px;
                background: #fafafa;
                cursor: pointer;
                transition: border-color 0.15s ease, background 0.15s ease;
            }

            .tpbh-lot:hover {
                border-color: #8a6db1;
                background: #f6f0ff;
            }

            .tpbh-lot-selected {
                border-color: #5e2ca5;
                background: #efe4ff;
            }

            .tpbh-check {
                margin-top: 4px;
                transform: scale(1.25);
            }

            .tpbh-lot-body {
                min-width: 0;
                flex: 1;
            }

            .tpbh-lot-top {
                display: flex;
                justify-content: space-between;
                gap: 8px;
                font-size: 13px;
                color: #222;
                margin-bottom: 4px;
            }

            .tpbh-lot-title {
                font-weight: 800;
                color: #222;
                font-size: 14px;
                margin-bottom: 4px;
                overflow-wrap: anywhere;
            }

            .tpbh-lot-wish {
                font-size: 12px;
                color: #444;
                margin-bottom: 4px;
                overflow-wrap: anywhere;
            }

            .tpbh-lot-meta {
                display: flex;
                flex-wrap: wrap;
                gap: 8px;
                font-size: 12px;
                color: #555;
            }

            .tpbh-empty {
                padding: 14px;
                border: 1px dashed #bbb;
                border-radius: 8px;
                color: #555;
                background: #fafafa;
            }

            @media (max-width: 600px) {
                #tpbh-panel {
                    margin: 10px;
                    padding: 10px;
                }

                .tpbh-header {
                    flex-direction: column;
                    align-items: stretch;
                }

                .tpbh-actions {
                    flex-direction: column;
                }

                .tpbh-btn {
                    width: 100%;
                }
            }
        `;

        document.head.appendChild(style);
    }
})();