Last active
June 6, 2026 18:08
-
-
Save curreta/1be327aaaf04ad2964339734ccfc4a7f to your computer and use it in GitHub Desktop.
Poshmark live-show editor: bulk-select tiles by Title from a Blazer CSV (Tampermonkey/Violentmonkey userscript)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Poshmark Live Show Selector (Blazer CSV) | |
| // @namespace https://github.com/GarbsNET | |
| // @version 1.2 | |
| // @description Paste a Blazer CSV export and bulk-select matching tiles in the Poshmark live-show editor by Title | |
| // @match https://poshmark.com/* | |
| // @match https://www.poshmark.com/* | |
| // @grant none | |
| // @run-at document-idle | |
| // @updateURL https://gist.githubusercontent.com/curreta/1be327aaaf04ad2964339734ccfc4a7f/raw/poshmark-show-selector.user.js | |
| // @downloadURL https://gist.githubusercontent.com/curreta/1be327aaaf04ad2964339734ccfc4a7f/raw/poshmark-show-selector.user.js | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| const LOG = '[Garbs Show Selector]'; | |
| const STORAGE_KEY = 'garbs_show_selector_csv'; | |
| // --- CSV (RFC-4180-ish): handles quoted fields, embedded commas/quotes/newlines --- | |
| function parseCSV(text) { | |
| const rows = []; | |
| let row = [], field = '', inQuotes = false; | |
| for (let i = 0; i < text.length; i++) { | |
| const c = text[i]; | |
| if (inQuotes) { | |
| if (c === '"') { | |
| if (text[i + 1] === '"') { field += '"'; i++; } | |
| else inQuotes = false; | |
| } else { | |
| field += c; | |
| } | |
| } else if (c === '"') { | |
| inQuotes = true; | |
| } else if (c === ',') { | |
| row.push(field); field = ''; | |
| } else if (c === '\r') { | |
| // ignore; handled by \n | |
| } else if (c === '\n') { | |
| row.push(field); rows.push(row); row = []; field = ''; | |
| } else { | |
| field += c; | |
| } | |
| } | |
| if (field.length > 0 || row.length > 0) { row.push(field); rows.push(row); } | |
| return rows; | |
| } | |
| // Pull the Title column from a Blazer CSV. Falls back to 2nd column if no header match. | |
| function extractTitles(csvText) { | |
| const rows = parseCSV(csvText).filter(r => r.some(c => c.trim() !== '')); | |
| if (rows.length === 0) return []; | |
| const header = rows[0].map(h => h.trim().toLowerCase()); | |
| let titleIdx = header.indexOf('title'); | |
| let hasHeader = titleIdx !== -1; | |
| if (!hasHeader) titleIdx = 1; // Blazer query column order: Custom Label, Title, Location | |
| const body = hasHeader ? rows.slice(1) : rows; | |
| return body | |
| .map(r => (r[titleIdx] || '').trim()) | |
| .filter(Boolean); | |
| } | |
| const norm = s => (s || '').toLowerCase().replace(/\s+/g, ' ').trim(); | |
| // --- Tile reading --- | |
| function readTiles() { | |
| return Array.from(document.querySelectorAll('.tile-grid-redesign')).map(el => { | |
| const titleEl = el.querySelector('.tile-grid-redesign__title'); | |
| const link = el.querySelector('.tile__covershot') || el; | |
| return { | |
| el, | |
| clickTarget: link, | |
| title: titleEl ? titleEl.textContent : '', | |
| normTitle: norm(titleEl ? titleEl.textContent : ''), | |
| listingId: link.getAttribute('data-et-prop-listing_id') || el.getAttribute('data-et-prop-listing_id') || '' | |
| }; | |
| }); | |
| } | |
| // exact normalized match first, then substring fallback | |
| function findMatches(target, tiles) { | |
| const exact = tiles.filter(t => t.normTitle === target); | |
| if (exact.length) return exact; | |
| return tiles.filter(t => t.normTitle && t.normTitle.includes(target)); | |
| } | |
| const clicked = new Set(); // listingId (or normTitle fallback) of tiles we've already selected | |
| function tileKey(t) { | |
| return t.listingId || ('title:' + t.normTitle); | |
| } | |
| function selectTitles(titles) { | |
| const tiles = readTiles(); | |
| const uniqueTargets = [...new Set(titles.map(norm))].filter(Boolean); | |
| const result = { total: uniqueTargets.length, selected: 0, alreadyDone: 0, unmatched: [], dupes: [] }; | |
| uniqueTargets.forEach(target => { | |
| const matches = findMatches(target, tiles); | |
| if (matches.length === 0) { | |
| result.unmatched.push(target); | |
| return; | |
| } | |
| if (matches.length > 1) result.dupes.push({ target, count: matches.length }); | |
| matches.forEach(t => { | |
| const key = tileKey(t); | |
| if (clicked.has(key)) { result.alreadyDone++; return; } | |
| t.clickTarget.click(); | |
| clicked.add(key); | |
| result.selected++; | |
| }); | |
| }); | |
| return result; | |
| } | |
| // --- UI --- | |
| function buildPanel() { | |
| if (document.getElementById('garbs-show-selector')) return; | |
| const style = document.createElement('style'); | |
| style.textContent = ` | |
| #garbs-show-selector { | |
| position: fixed; bottom: 16px; right: 16px; z-index: 999999; | |
| width: 320px; font-family: -apple-system, system-ui, sans-serif; font-size: 13px; | |
| background: #fff; border: 1px solid #d0d0d0; border-radius: 8px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.18); overflow: hidden; | |
| } | |
| #garbs-show-selector .gss-head { | |
| display: flex; align-items: center; justify-content: space-between; | |
| background: #7f0353; color: #fff; padding: 8px 10px; cursor: default; font-weight: 600; | |
| } | |
| #garbs-show-selector .gss-head button { | |
| background: rgba(255,255,255,0.2); color: #fff; border: none; border-radius: 4px; | |
| width: 22px; height: 22px; cursor: pointer; font-size: 13px; line-height: 1; | |
| } | |
| #garbs-show-selector .gss-body { padding: 10px; } | |
| #garbs-show-selector.gss-collapsed .gss-body { display: none; } | |
| #garbs-show-selector textarea { | |
| width: 100%; height: 80px; box-sizing: border-box; resize: vertical; | |
| border: 1px solid #ccc; border-radius: 5px; padding: 6px; font-size: 12px; font-family: ui-monospace, monospace; | |
| } | |
| #garbs-show-selector .gss-row { display: flex; gap: 8px; align-items: center; margin-top: 8px; } | |
| #garbs-show-selector button.gss-btn { | |
| background: #7f0353; color: #fff; border: none; border-radius: 5px; padding: 7px 10px; | |
| cursor: pointer; font-weight: 600; font-size: 12px; | |
| } | |
| #garbs-show-selector button.gss-btn.secondary { background: #eee; color: #333; } | |
| #garbs-show-selector .gss-count { color: #555; } | |
| #garbs-show-selector .gss-report { margin-top: 10px; border-top: 1px solid #eee; padding-top: 8px; } | |
| #garbs-show-selector .gss-stat { font-weight: 600; } | |
| #garbs-show-selector .gss-warn { color: #b45309; } | |
| #garbs-show-selector .gss-miss-list { | |
| margin: 6px 0 0; padding: 6px; max-height: 140px; overflow: auto; | |
| background: #fafafa; border: 1px solid #eee; border-radius: 5px; font-size: 11px; | |
| } | |
| #garbs-show-selector .gss-miss-list div { padding: 1px 0; color: #444; } | |
| #garbs-show-selector .gss-link { color: #7f0353; cursor: pointer; text-decoration: underline; font-size: 11px; } | |
| `; | |
| document.head.appendChild(style); | |
| const panel = document.createElement('div'); | |
| panel.id = 'garbs-show-selector'; | |
| panel.innerHTML = ` | |
| <div class="gss-head"> | |
| <span>Garbs → Posh Selector</span> | |
| <button class="gss-toggle" title="Collapse">–</button> | |
| </div> | |
| <div class="gss-body"> | |
| <textarea class="gss-csv" placeholder="Paste Blazer CSV here (Custom Label, Title, Location)…"></textarea> | |
| <div class="gss-row"> | |
| <button class="gss-btn secondary gss-parse">Parse CSV</button> | |
| <span class="gss-count">0 titles</span> | |
| </div> | |
| <div class="gss-row"> | |
| <button class="gss-btn gss-select">Select tiles in view</button> | |
| <button class="gss-btn secondary gss-reset" title="Forget already-selected tiles">Reset</button> | |
| </div> | |
| <div class="gss-report" style="display:none"></div> | |
| </div> | |
| `; | |
| document.body.appendChild(panel); | |
| const $ = sel => panel.querySelector(sel); | |
| const csvEl = $('.gss-csv'); | |
| const countEl = $('.gss-count'); | |
| const reportEl = $('.gss-report'); | |
| let titles = []; | |
| // restore last CSV | |
| const saved = localStorage.getItem(STORAGE_KEY); | |
| if (saved) { csvEl.value = saved; } | |
| function refreshCount() { | |
| titles = extractTitles(csvEl.value); | |
| const uniq = new Set(titles.map(norm)).size; | |
| countEl.textContent = `${titles.length} titles (${uniq} unique)`; | |
| } | |
| if (saved) refreshCount(); | |
| $('.gss-toggle').addEventListener('click', () => { | |
| panel.classList.toggle('gss-collapsed'); | |
| $('.gss-toggle').textContent = panel.classList.contains('gss-collapsed') ? '+' : '–'; | |
| }); | |
| $('.gss-parse').addEventListener('click', () => { | |
| localStorage.setItem(STORAGE_KEY, csvEl.value); | |
| refreshCount(); | |
| reportEl.style.display = 'none'; | |
| }); | |
| $('.gss-reset').addEventListener('click', () => { | |
| clicked.clear(); | |
| reportEl.innerHTML = '<span class="gss-stat">Cleared selection memory.</span>'; | |
| reportEl.style.display = 'block'; | |
| console.log(LOG, 'Cleared clicked-tile memory'); | |
| }); | |
| $('.gss-select').addEventListener('click', () => { | |
| if (!titles.length) refreshCount(); | |
| if (!titles.length) { | |
| reportEl.innerHTML = '<span class="gss-warn">No titles parsed. Paste CSV and Parse first.</span>'; | |
| reportEl.style.display = 'block'; | |
| return; | |
| } | |
| const r = selectTitles(titles); | |
| console.log(LOG, 'Select run:', r); | |
| let html = `<div class="gss-stat">Selected ${r.selected} / ${r.total}` + | |
| (r.alreadyDone ? ` <span class="gss-count">(${r.alreadyDone} already selected)</span>` : '') + `</div>`; | |
| if (r.dupes.length) { | |
| html += `<div class="gss-warn">⚠ ${r.dupes.length} title(s) matched multiple tiles</div>`; | |
| } | |
| if (r.unmatched.length) { | |
| html += `<div class="gss-warn">Unmatched ${r.unmatched.length} ` + | |
| `<span class="gss-link gss-copy">⧉ copy</span></div>`; | |
| html += `<div class="gss-miss-list">` + | |
| r.unmatched.map(t => `<div>${t.replace(/</g, '<')}</div>`).join('') + `</div>`; | |
| html += `<div class="gss-count" style="margin-top:4px">Not in viewport? Scroll the grid and click Select again.</div>`; | |
| } else { | |
| html += `<div class="gss-stat" style="color:#15803d">All titles matched ✓</div>`; | |
| } | |
| reportEl.innerHTML = html; | |
| reportEl.style.display = 'block'; | |
| const copyBtn = reportEl.querySelector('.gss-copy'); | |
| if (copyBtn) { | |
| copyBtn.addEventListener('click', () => { | |
| navigator.clipboard.writeText(r.unmatched.join('\n')); | |
| copyBtn.textContent = '✓ copied'; | |
| }); | |
| } | |
| }); | |
| console.log(LOG, 'Panel mounted'); | |
| } | |
| // Mount only when the live-show editor grid is present (auto-scopes without needing the URL). | |
| function isEditorPresent() { | |
| return !!document.querySelector('.tile__checkbox-overlay--live-show-editor') || | |
| !!document.querySelector('.tile-grid-redesign'); | |
| } | |
| function watchForEditor() { | |
| if (isEditorPresent()) buildPanel(); | |
| const obs = new MutationObserver(() => { | |
| if (isEditorPresent() && !document.getElementById('garbs-show-selector')) { | |
| buildPanel(); | |
| } | |
| }); | |
| obs.observe(document.body, { childList: true, subtree: true }); | |
| } | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', watchForEditor); | |
| } else { | |
| watchForEditor(); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment