# Marketplace Tags — Extensão (Chrome/Edge) **O que é:** Uma extensão leve que adiciona um painel de **etiquetas coloridas** diretamente nas conversas do Facebook Marketplace/Messenger no navegador. As etiquetas são salvas localmente (chrome.storage.local) e ficam disponíveis somente no seu navegador. > Arquivos incluídos no pacote (prontos para zipar e instalar via "Load unpacked"): --- ## `manifest.json` ```json { "manifest_version": 3, "name": "Marketplace Tags", "version": "1.0", "description": "Adiciona etiquetas coloridas nas conversas do Facebook Marketplace.", "permissions": ["storage", "activeTab", "scripting"], "host_permissions": ["https://www.facebook.com/*"], "icons": { "48": "icons/icon48.png", "128": "icons/icon128.png" }, "content_scripts": [ { "matches": ["https://www.facebook.com/*"], "js": ["content_script.js"], "css": ["styles.css"], "run_at": "document_idle" } ], "action": { "default_popup": "popup.html", "default_icon": "icons/icon48.png" } } ``` --- ## `content_script.js` ```javascript (() => { // Tags predefinidas (id, label, color) const TAGS = [ { id: 'novo', label: 'Novo contato', color: '#28a745' }, { id: 'negociando', label: 'Negociando', color: '#ffc107' }, { id: 'aguardando', label: 'Aguardando resposta', color: '#007bff' }, { id: 'fechado', label: 'Venda fechada', color: '#dc3545' }, { id: 'sem_interesse', label: 'Sem interesse', color: '#6c757d' } ]; const STORAGE_KEY = 'marketplace_tags_v1'; let currentThreadId = null; function log(...args){ /*console.log('[MTags]', ...args)*/ } function getThreadIdFromUrl() { // tenta extrair thread id do padrão /messages/t/ID const m = location.href.match(/\/messages\/t\/([^/?#&]+)/); if (m) return m[1]; // fallback: usa a url completa (menos params) como id return location.href.split(/[?#]/)[0]; } // Carrega mapa de tags do storage function loadAllTags() { return new Promise((resolve) => { try { chrome.storage.local.get([STORAGE_KEY], (res) => { resolve(res[STORAGE_KEY] || {}); }); } catch (e) { resolve({}); } }); } function saveAllTags(map) { return new Promise((resolve) => { const obj = {}; obj[STORAGE_KEY] = map; try { chrome.storage.local.set(obj, () => resolve()); } catch (e) { resolve(); } }); } async function getTagForThread(threadId) { const map = await loadAllTags(); return map[threadId] || null; } async function setTagForThread(threadId, tagId) { const map = await loadAllTags(); if (tagId) map[threadId] = tagId; else delete map[threadId]; await saveAllTags(map); } // UI function createBar() { if (document.getElementById('mktags-bar')) return; const bar = document.createElement('div'); bar.id = 'mktags-bar'; bar.className = 'mktags-bar'; const title = document.createElement('div'); title.className = 'mktags-title'; title.innerText = 'Etiquetas'; bar.appendChild(title); const buttons = document.createElement('div'); buttons.className = 'mktags-buttons'; TAGS.forEach(t => { const b = document.createElement('button'); b.className = 'mktags-btn'; b.dataset.tag = t.id; b.title = t.label; b.innerText = t.label; b.style.borderLeft = `6px solid ${t.color}`; b.addEventListener('click', async (e) => { e.preventDefault(); await setTagForThread(currentThreadId, t.id); updateSelected(); }); buttons.appendChild(b); }); // botão para remover etiqueta const clear = document.createElement('button'); clear.className = 'mktags-clear'; clear.innerText = 'Remover etiqueta'; clear.addEventListener('click', async () => { await setTagForThread(currentThreadId, null); updateSelected(); }); bar.appendChild(buttons); bar.appendChild(clear); // botão abrir lista de tags salvas const listBtn = document.createElement('button'); listBtn.className = 'mktags-listbtn'; listBtn.innerText = 'Ver todas etiquetas'; listBtn.addEventListener('click', showTagsListOverlay); bar.appendChild(listBtn); document.body.appendChild(bar); } async function updateSelected() { const tag = await getTagForThread(currentThreadId); document.querySelectorAll('.mktags-btn').forEach(b => { if (b.dataset.tag === tag) b.classList.add('selected'); else b.classList.remove('selected'); }); } async function showTagsListOverlay() { const existing = document.getElementById('mktags-overlay'); if (existing) { existing.remove(); return; } const map = await loadAllTags(); const overlay = document.createElement('div'); overlay.id = 'mktags-overlay'; overlay.className = 'mktags-overlay'; const box = document.createElement('div'); box.className = 'mktags-box'; const h = document.createElement('h3'); h.innerText = 'Todas etiquetas salvas'; box.appendChild(h); const list = document.createElement('div'); list.className = 'mktags-list'; const entries = Object.entries(map); if (entries.length === 0) { const p = document.createElement('p'); p.innerText = 'Nenhuma etiqueta salva.'; list.appendChild(p); } else { entries.forEach(([threadId, tagId]) => { const row = document.createElement('div'); row.className = 'mktags-list-row'; const tagObj = TAGS.find(x => x.id === tagId) || { label: tagId, color: '#444' }; const color = document.createElement('span'); color.className = 'mktags-list-color'; color.style.background = tagObj.color; row.appendChild(color); const a = document.createElement('a'); a.href = threadId; a.target = '_blank'; a.innerText = (threadId.length>60? threadId.substr(0,57)+'...': threadId); row.appendChild(a); const tagLabel = document.createElement('span'); tagLabel.className = 'mktags-list-tag'; tagLabel.innerText = tagObj.label; row.appendChild(tagLabel); list.appendChild(row); }); } box.appendChild(list); const close = document.createElement('button'); close.className = 'mktags-close'; close.innerText = 'Fechar'; close.addEventListener('click', () => overlay.remove()); box.appendChild(close); overlay.appendChild(box); document.body.appendChild(overlay); } // Observa mudanças de rota (single page app do FB) function observeRouteChanges() { let last = location.href; const mo = new MutationObserver(() => { if (location.href !== last) { last = location.href; onRouteChange(); } }); mo.observe(document, { subtree: true, childList: true }); } async function onRouteChange() { currentThreadId = getThreadIdFromUrl(); // cria a barra se ainda não createBar(); // atualiza o botão selecionado updateSelected(); } // inicialização com leve atraso function init() { try { createBar(); currentThreadId = getThreadIdFromUrl(); updateSelected(); observeRouteChanges(); } catch (e) { log('init error', e); } } // aguarda o carregamento inicial do DOM if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else setTimeout(init, 1200); })(); ``` --- ## `styles.css` ```css /* Estilos simples para a barra de etiquetas */ .mktags-bar { position: fixed; right: 12px; top: 120px; width: 220px; background: rgba(255,255,255,0.98); border: 1px solid #ddd; box-shadow: 0 6px 18px rgba(0,0,0,0.08); padding: 8px; z-index: 9999999; border-radius: 8px; font-family: Arial, sans-serif; } .mktags-title { font-weight: 700; margin-bottom: 6px; } .mktags-buttons { display:flex; flex-direction:column; gap:6px; margin-bottom:8px; } .mktags-btn { background: #fff; border: 1px solid #eee; padding:6px 8px; text-align:left; cursor:pointer; border-radius:6px; font-size:13px; } .mktags-btn.selected { box-shadow: inset 0 0 0 2px rgba(0,0,0,0.06); } .mktags-clear { width:100%; padding:6px; margin-top:6px; background:#f8f9fa; border-radius:6px; border:1px solid #e9ecef; cursor:pointer; } .mktags-listbtn { margin-top:6px; width:100%; padding:6px; background:#e9ecef; border-radius:6px; border:1px solid #ddd; cursor:pointer; } /* overlay */ .mktags-overlay { position: fixed; inset:0; background: rgba(0,0,0,0.45); z-index: 10000000; display:flex; align-items:center; justify-content:center; } .mktags-box { background:#fff; padding:16px; border-radius:8px; width:720px; max-height:80vh; overflow:auto; } .mktags-list-row { display:flex; align-items:center; gap:12px; margin:8px 0; } .mktags-list-color { width:16px; height:16px; border-radius:4px; display:inline-block; } .mktags-list-tag { margin-left:auto; font-weight:600; } .mktags-close { margin-top:12px; padding:8px 12px; cursor:pointer; } ``` --- ## `popup.html` ```html
Extensão simples para marcar conversas no Facebook Marketplace.
Observações: