TCGPlayer Cart/History to CSV in Console

Takes a TCGPlayer Cart/History and spits out a CSV

// ==UserScript==
// @name         TCGPlayer Cart/History to CSV in Console
// @namespace    http://tampermonkey.net/
// @version      10-01-2025
// @description  Takes a TCGPlayer Cart/History and spits out a CSV
// @author       multimeric, ganondorc, micool777
// @match        https://cart.tcgplayer.com/*
// @match        https://store.tcgplayer.com/myaccount/orderhistory
// @match        https://store.tcgplayer.com/shoppingcart/review
// @match        https://www.tcgplayer.com/checkout
// @match        https://www.tcgplayer.com/cart
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tcgplayer.com
// @grant        none
// @require      http://code.jquery.com/jquery-3.4.1.min.js
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  /* -------------------- helpers -------------------- */
  function createButtonRow(buttonText, onClickHandler, testid) {
    const row = document.createElement('div');
    row.className = 'button-row';
    row.style.paddingBottom = '5px';
    row.style.paddingTop = '5px';

    const button = document.createElement('button');
    button.className = 'tcg-button tcg-button--md tcg-standard-button tcg-standard-button--priority is-full-width checkout-btn';
    button.type = 'button';
    button.dataset.testid = testid;

    const buttonContent = document.createElement('span');
    buttonContent.className = 'tcg-standard-button__content';
    const spanText = document.createElement('span');
    spanText.textContent = buttonText;
    buttonContent.appendChild(spanText);
    button.appendChild(buttonContent);

    button.addEventListener('click', onClickHandler);
    row.appendChild(button);
    return row;
  }

  const $$ = (root, sel) => Array.from(root.querySelectorAll(sel));

  /* -------------------- CSV: order history (unchanged) -------------------- */
  function generateOrderHistoryCSV() {
    let csv = 'Name,Rarity,Condition,Individual Price,Quantity\n';
    for (let order of $$(document, '.orderWrap')) {
      for (let tr of $$(order, '.orderTable tr')) {
        try {
          const name = '"' + $$(tr, '.nocontext')[0].innerText.trim() + '"';
          const details = $$(tr, '.orderHistoryDetail')[0];
          const rarity = details.childNodes[0].textContent.split(':')[1];
          const condition = details.childNodes[2].textContent.split(':')[1];
          const price = $$(tr, '.orderHistoryPrice')[0].innerText;
          const qty = $$(tr, '.orderHistoryQuantity')[0].innerText;
          csv += [name, rarity, condition, price, qty].map(s => (s || '').trim()).join(',') + '\n';
        } catch { /* skip non-item rows */ }
      }
    }
    return csv;
  }

  /* -------------------- CSV: cart/checkout (NEW DOM first, fallback to old) -------------------- */
  function generateCartCSV_NewCheckout() {
    // Target the structure in your snippet: ul.item-list > li.list-item > section.package-item ...
    const items = $$(document, 'ul.item-list li.list-item section.package-item section.content .description');
    if (!items.length) return null;

    let csv = 'Name,Set,Rarity,Condition,Individual Price,Quantity\n';

    for (const desc of items) {
      try {
        const nameEl = desc.querySelector('p.name[data-testid="productName"]');
        const name = nameEl ? `"${nameEl.textContent.trim()}"` : '""';

        const metaRoot = desc.querySelector('[data-testid="areaMetadataDropdown"]');
        const setName = metaRoot?.querySelector('.display-text span')?.textContent?.trim() || '';
        const rarity = (metaRoot?.querySelector('.expand-items li:nth-child(2)')?.textContent || '').trim();

        const condition = desc.querySelector('p.condition[data-testid="txtItemCondition"]')?.textContent?.trim() || '';
        const price = desc.querySelector('p.price span.checkout-price')?.textContent?.trim()?.replace(/\$/g, '') || '';

        // Quantity isn't exposed in your snippet at checkout; default to 1.
        const qty = '1';

        csv += [name, setName, rarity, condition, price, qty].join(',') + '\n';
      } catch (e) {
        // continue on any odd row
      }
    }

    return csv;
  }

  function generateCartCSV_OldCart() {
    // Legacy cart (sellerWrapMarket) fallback
    let tables = Array.from($('.sellerWrapMarket'));
    if (!tables.length) return null;

    let csv = 'Name,Set,Rarity,Condition,Individual Price,Quantity\n';
    for (let table of tables) {
      let $table = $(table);
      let rows = Array.from($table.find('table.sellerTable'));
      for (let row of rows) {
        let $row = $(row);
        const name = $row.find('.itemsContents h3').text().replace(/ *\([^)]*\) */g, '').trim();
        const price = $row.find('.priceBox').text().trim().replace(/\$/g, '');
        if (name) {
          // These legacy rows didn't surface set/rarity/condition consistently; fill what we have.
          csv += [`"${name}"`, '', '', '', price, '1'].join(',') + '\n';
        }
      }
    }
    return csv;
  }

  function generateCartCSV() {
    // Try the new checkout DOM first; if not found, fall back to old cart DOM.
    return generateCartCSV_NewCheckout() || generateCartCSV_OldCart() || '';
  }

  /* -------------------- export + clipboard -------------------- */
  async function exportCsvToConsoleAndClipboard() {
    const onHistory = location.href.includes('/myaccount/orderhistory');
    const onCartLike =
      location.href.includes('/cart') ||
      location.href.includes('/checkout') ||
      location.href.includes('/shoppingcart/review') ||
      location.href.includes('cart.tcgplayer.com');

    let parts = [];

    if (onHistory) {
      const historyCsv = generateOrderHistoryCSV();
      if (historyCsv && historyCsv.split('\n').length > 1) {
        console.log('[TCG CSV] Order History CSV:\n', historyCsv);
        parts.push(historyCsv);
      }
    }

    if (onCartLike) {
      const cartCsv = generateCartCSV();
      if (cartCsv && cartCsv.split('\n').length > 1) {
        console.log('[TCG CSV] Cart/Checkout CSV:\n', cartCsv);
        parts.push(cartCsv);
      }
    }

    if (!parts.length) {
      console.warn('[TCG CSV] No CSV data found for this page.');
      window.alert('No CSV data found on this page.');
      return;
    }

    const combined = parts.join('\n');

    try {
      if (navigator.clipboard && window.isSecureContext) {
        await navigator.clipboard.writeText(combined);
        window.alert('CSV generated, logged to console, and copied to clipboard.');
      } else {
        window.alert('CSV generated and logged to console.\n(Clipboard unavailable in this context.)');
      }
    } catch (e) {
      console.warn('Clipboard write failed:', e);
      window.alert('CSV generated and logged to console.\n(Clipboard copy failed.)');
    }
  }

  /* -------------------- button injection under “Place Order” -------------------- */
  function findCheckoutButton() {
    // Typical class
    let btn = document.querySelector('section.cart-summary .checkout-btn') ||
              document.querySelector('.checkout-btn');

    // Fallback: match visible text "Place Order"
    if (!btn) {
      const spans = $$(document, '.tcg-standard-button__content span');
      const match = spans.find(s => s.textContent.trim().toLowerCase() === 'place order');
      if (match) btn = match.closest('button');
    }

    // Extra fallback: sometimes testid is present
    if (!btn) {
      btn = document.querySelector('button[data-testid="btnPlaceOrder"]');
    }

    return btn;
  }

  function injectExportButton() {
    const checkoutBtn = findCheckoutButton();
    if (!checkoutBtn) return;

    if (document.querySelector('button[data-testid="btnExportCartHistoryCsv"]')) return;

    const exportRow = createButtonRow(
      'Export Cart/History CSV',
      exportCsvToConsoleAndClipboard,
      'btnExportCartHistoryCsv'
    );

    // Insert directly UNDER the Place Order button
    checkoutBtn.insertAdjacentElement('afterend', exportRow);
  }

  function setupObserver() {
    const observer = new MutationObserver(injectExportButton);
    observer.observe(document.body, { childList: true, subtree: true });
  }

  /* -------------------- init -------------------- */
  window.addEventListener('load', () => {
    setupObserver();
    injectExportButton();
  });
})();