Skip to content

Instantly share code, notes, and snippets.

@curreta
Last active June 6, 2026 18:08
Show Gist options
  • Select an option

  • Save curreta/1be327aaaf04ad2964339734ccfc4a7f to your computer and use it in GitHub Desktop.

Select an option

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)
// ==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, '&lt;')}</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