Shopee – Filtros e Ordenação Local

Filtra AND|OR|NOT e ordena client-side os produtos já carregados, sem infinite scroll. SPA-aware. Limpa filtros e usa preço lado a lado.

// ==UserScript==
// @name         Shopee – Filtros e Ordenação Local
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  Filtra AND|OR|NOT e ordena client-side os produtos já carregados, sem infinite scroll. SPA-aware. Limpa filtros e usa preço lado a lado.
// @author       Você
// @match        https://shopee.com.br/*
// @match        https://shopee.com/*
// @grant        none
// ==/UserScript==
(function(){
  'use strict';

  const containerSelector = 'ul.shopee-search-item-result__items';

  // --- Painel de filtros e ordenação ---
  const panel = document.createElement('div');
  panel.id = 'shopee-filter-panel';
  panel.innerHTML = `
    <div id="shopee-filter-header">
      <span>Filtros Shopee</span>
      <button id="shopee-filter-clear" title="Limpar filtros">✕</button>
    </div>
    <div id="shopee-filter-content">
      <div class="shopee-filter-row">
        <label>Palavras (AND implícito, "|"=OR, "!"=NOT):</label>
        <input type="text" id="shopee-filter-keywords" placeholder="ex: papel kit|folhas !glossy">
      </div>
      <div class="shopee-filter-row price-row">
        <label>Preço de (R$):</label>
        <input type="number" id="shopee-filter-minprice" placeholder="0" min="0">
        <label>até:</label>
        <input type="number" id="shopee-filter-maxprice" placeholder="0" min="0">
      </div>
      <div class="shopee-filter-row">
        <label>Ordenar:</label>
        <select id="shopee-filter-sort">
          <option value="">Relevância</option>
          <option value="sales,desc">Mais Vendidos</option>
          <option value="price,asc">Menor Preço</option>
          <option value="price,desc">Maior Preço</option>
        </select>
      </div>
    </div>`;
  document.head.insertAdjacentHTML('beforeend',`
    <style>
      #shopee-filter-panel {
        position: fixed; bottom: 20px; right: 20px; width: 300px;
        background: #fff; border: 1px solid #ccc; box-shadow: 0 2px 8px rgba(0,0,0,.15);
        z-index: 9999; font-family: Arial,sans-serif;
      }
      #shopee-filter-header {
        display: flex; justify-content: space-between; align-items: center;
        padding: 8px; background: #f5f5f5; font-weight: bold;
      }
      #shopee-filter-clear {
        background: none; border: none; font-size: 16px; cursor: pointer;
      }
      #shopee-filter-content { padding: 8px; }
      .shopee-filter-row { margin-bottom: 8px; }
      .shopee-filter-row label {
        display: block; font-size: 12px; margin-bottom: 4px;
      }
      .shopee-filter-row input,
      .shopee-filter-row select {
        width: 100%; padding: 4px; font-size: 12px; box-sizing: border-box;
      }
      .price-row {
        display: flex; align-items: center; gap: 4px;
      }
      .price-row label {
        margin-bottom: 0; white-space: nowrap; font-size: 12px;
      }
      .price-row input { flex: 1; }
      .shopee-filter-hidden { display: none !important; }
    </style>`);
  document.body.appendChild(panel);

  // --- Função de filtragem AND|OR|NOT + preço ---
  function filterProducts() {
    const raw = document.getElementById('shopee-filter-keywords').value
                   .toLowerCase().trim();
    const min = parseFloat(document.getElementById('shopee-filter-minprice').value) || null;
    const max = parseFloat(document.getElementById('shopee-filter-maxprice').value) || null;
    const tokens = raw.split(/\s+/).filter(Boolean);
    const req = [], forbid = [], groups = [];
    tokens.forEach(tok => {
      if (tok.startsWith('!')) forbid.push(tok.slice(1));
      else if (tok.includes('|')) groups.push(tok.split('|'));
      else req.push(tok);
    });

    document.querySelectorAll(`${containerSelector} .shopee-search-item-result__item`)
      .forEach(li => {
        const title = (li.querySelector('.line-clamp-2')?.innerText || '')
                        .toLowerCase();
        let price = null;
        const base = li.querySelector('.flex.items-baseline');
        if (base && base.querySelectorAll('span')[1]) {
          price = parseFloat(
            base.querySelectorAll('span')[1].innerText
                .replace(/\./g,'').replace(',','.')
          );
        }
        let hide = false;
        forbid.forEach(k => { if (title.includes(k)) hide = true; });
        req.forEach(k => { if (!title.includes(k)) hide = true; });
        groups.forEach(arr => { if (!arr.some(k => title.includes(k))) hide = true; });
        if (price !== null) {
          if (min !== null && price < min) hide = true;
          if (max !== null && max > 0 && price > max) hide = true;
        }
        li.classList[hide ? 'add' : 'remove']('shopee-filter-hidden');
      });
  }

  // --- Ordenação client-side ---
  function sortProducts() {
    const sel = document.getElementById('shopee-filter-sort').value;
    const cont = document.querySelector(containerSelector);
    if (!sel || !cont) return;
    const [key, dir] = sel.split(',');
    const items = Array.from(cont.querySelectorAll('.shopee-search-item-result__item'))
      .filter(li => !li.classList.contains('shopee-filter-hidden'));

    function getVal(li) {
      if (key === 'price') {
        const b = li.querySelector('.flex.items-baseline');
        return b && b.querySelectorAll('span')[1]
          ? parseFloat(b.querySelectorAll('span')[1].innerText
                         .replace(/\./g,'').replace(',','.'))
          : 0;
      }
      if (key === 'sales') {
        const sold = li.querySelector('.flex.items-center .truncate:nth-child(3)')?.innerText || '0';
        let n = sold.replace(/\./g,'').replace(/mil/i,'000').replace(/\D+/g,'');
        return parseInt(n) || 0;
      }
      return 0;
    }

    items.sort((a, b) => {
      const va = getVal(a), vb = getVal(b);
      return dir === 'asc' ? va - vb : vb - va;
    });
    items.forEach(li => cont.appendChild(li));
  }

  // --- Botão limpar filtros ---
  document.getElementById('shopee-filter-clear').addEventListener('click', () => {
    ['keywords','minprice','maxprice'].forEach(id => {
      document.getElementById('shopee-filter-' + id).value = '';
    });
    filterProducts();
    sortProducts();
  });

  // --- Eventos de input/change para filtros e ordenação ---
  ['keywords','minprice','maxprice'].forEach(id => {
    ['input','change'].forEach(evt => {
      document.getElementById('shopee-filter-' + id)
        .addEventListener(evt, () => { filterProducts(); sortProducts(); });
    });
  });
  document.getElementById('shopee-filter-sort')
    .addEventListener('change', sortProducts);

  // --- SPA-aware: re-aplica ao mudar de página (pushState/popState) ---
  function onLocationChange(){
    filterProducts();
    sortProducts();
  }
  ['pushState','replaceState'].forEach(fn => {
    const orig = history[fn];
    history[fn] = function(){
      const rv = orig.apply(this, arguments);
      window.dispatchEvent(new Event('locationchange'));
      return rv;
    };
  });
  window.addEventListener('popstate', () => window.dispatchEvent(new Event('locationchange')));
  window.addEventListener('locationchange', onLocationChange);

  // --- Inicialização ---
  onLocationChange();

})();