← back to Govarbitrage

extension/popup.js

199 lines

/**
 * GovArbitrage Capture — popup controller.
 *
 * Responsibilities:
 *   - load/save the API base URL to chrome.storage.local
 *   - on "Capture", inject content.js's extractor into the active tab and show
 *     the returned object
 *   - on "Send", POST the captured object to `${apiBase}/api/import/extension`
 *
 * All async paths are guarded so a failure surfaces as readable status text
 * rather than an unhandled rejection.
 */

const DEFAULT_API_BASE = 'http://localhost:3000';
const STORAGE_KEY = 'govarbitrage_api_base';
const IMPORT_PATH = '/api/import/extension';

const els = {
  apiBase: document.getElementById('apiBase'),
  captureBtn: document.getElementById('captureBtn'),
  sendBtn: document.getElementById('sendBtn'),
  preview: document.getElementById('preview'),
  summary: document.getElementById('summary'),
  status: document.getElementById('status'),
};

/** Last successfully captured listing object (or null). */
let captured = null;

// ------------------------------------------------------------------ status
function setStatus(msg, kind = 'info') {
  els.status.textContent = msg || '';
  els.status.className = kind;
}

// ------------------------------------------------------------- storage load
function loadApiBase() {
  try {
    chrome.storage.local.get([STORAGE_KEY], (res) => {
      const val = (res && res[STORAGE_KEY]) || DEFAULT_API_BASE;
      els.apiBase.value = val;
    });
  } catch (_e) {
    els.apiBase.value = DEFAULT_API_BASE;
  }
}

function saveApiBase() {
  const val = normalizeBase(els.apiBase.value);
  try {
    chrome.storage.local.set({ [STORAGE_KEY]: val });
  } catch (_e) {
    /* non-fatal */
  }
}

function normalizeBase(v) {
  let base = (v || '').trim() || DEFAULT_API_BASE;
  // strip trailing slashes
  base = base.replace(/\/+$/, '');
  return base;
}

// ------------------------------------------------------------- active tab
function getActiveTab() {
  return new Promise((resolve, reject) => {
    try {
      chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
        if (chrome.runtime.lastError) return reject(new Error(chrome.runtime.lastError.message));
        const tab = tabs && tabs[0];
        if (!tab) return reject(new Error('No active tab found.'));
        resolve(tab);
      });
    } catch (e) {
      reject(e);
    }
  });
}

// ------------------------------------------------------------------ capture
async function onCapture() {
  setStatus('Capturing…', 'info');
  els.sendBtn.disabled = true;
  captured = null;

  let tab;
  try {
    tab = await getActiveTab();
  } catch (e) {
    setStatus('Could not read the active tab: ' + e.message, 'err');
    return;
  }

  if (!tab.id || !/^https?:/i.test(tab.url || '')) {
    setStatus('This page cannot be captured (only http/https pages).', 'err');
    return;
  }

  try {
    const results = await chrome.scripting.executeScript({
      target: { tabId: tab.id },
      // The extractor is defined in content.js; injecting the file makes
      // extractGovArbitrageListing available in the page, then we call it.
      files: ['content.js'],
    });

    // File injection returns undefined results, so run the call in a second
    // pass now that the function is defined in the page.
    const callResults = await chrome.scripting.executeScript({
      target: { tabId: tab.id },
      func: () =>
        typeof extractGovArbitrageListing === 'function'
          ? extractGovArbitrageListing()
          : null,
    });

    const data = callResults && callResults[0] && callResults[0].result;
    if (!data) {
      setStatus('Extraction returned no data on this page.', 'err');
      els.preview.textContent = '';
      els.summary.textContent = '';
      return;
    }

    captured = data;
    renderPreview(data);
    els.sendBtn.disabled = false;
    setStatus('Captured. Review, then send.', 'ok');
  } catch (e) {
    setStatus('Capture failed: ' + (e && e.message ? e.message : String(e)), 'err');
    els.preview.textContent = '';
    els.summary.textContent = '';
  }
}

function renderPreview(data) {
  try {
    els.preview.textContent = JSON.stringify(data, null, 2);
  } catch (_e) {
    els.preview.textContent = String(data);
  }
  const imgCount = Array.isArray(data.imageUrls) ? data.imageUrls.length : 0;
  els.summary.textContent = `${data.source} · #${data.sourceAuctionId || '—'} · ${imgCount} img`;
}

// --------------------------------------------------------------------- send
async function onSend() {
  if (!captured) {
    setStatus('Nothing captured yet.', 'err');
    return;
  }
  const base = normalizeBase(els.apiBase.value);
  saveApiBase();
  const endpoint = base + IMPORT_PATH;

  setStatus('Sending to ' + endpoint + ' …', 'info');
  els.sendBtn.disabled = true;

  try {
    const resp = await fetch(endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(captured),
    });

    let payload = null;
    const raw = await resp.text();
    try {
      payload = raw ? JSON.parse(raw) : null;
    } catch (_e) {
      payload = { raw };
    }

    if (resp.ok) {
      const id = payload && (payload.id || (payload.listing && payload.listing.id));
      setStatus('Sent ✓' + (id ? ' — listing ' + id : ' (HTTP ' + resp.status + ')'), 'ok');
    } else {
      const detail =
        (payload && (payload.error || payload.message)) || 'HTTP ' + resp.status;
      setStatus('Server rejected it: ' + detail, 'err');
      els.sendBtn.disabled = false;
    }
  } catch (e) {
    setStatus(
      'Network error — is the app running at ' + base + '? (' + (e && e.message ? e.message : e) + ')',
      'err'
    );
    els.sendBtn.disabled = false;
  }
}

// -------------------------------------------------------------------- wiring
els.captureBtn.addEventListener('click', onCapture);
els.sendBtn.addEventListener('click', onSend);
els.apiBase.addEventListener('change', saveApiBase);
els.apiBase.addEventListener('blur', saveApiBase);

loadApiBase();