Last active
December 25, 2025 20:25
-
-
Save ehartford/acd2529940ec3f5abcd4cdb8d255d9ef to your computer and use it in GitHub Desktop.
ViolentMonkeyMicroCenterStockFinder
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 Micro Center Stock Finder | |
| // @namespace violentmonkey-microcenter-stock-finder | |
| // @match https://www.microcenter.com/product/* | |
| // @grant GM_xmlhttpRequest | |
| // @connect www.microcenter.com | |
| // @version 1.7 | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| if (document.getElementById('mcFindStockBtn')) return; | |
| /* ------------------------------ | |
| * Utilities | |
| * ------------------------------ */ | |
| const sleep = ms => new Promise(r => setTimeout(r, ms)); | |
| function parseStockFromHTML(html) { | |
| const doc = new DOMParser().parseFromString(html, 'text/html'); | |
| const panel = doc.querySelector('#pnlInventory'); | |
| if (!panel) return { available: false, reason: 'No inventory panel' }; | |
| const text = panel.innerText.toLowerCase(); | |
| if ( | |
| text.includes('sold out') || | |
| text.includes('out of stock') || | |
| text.includes('currently unavailable') || | |
| text.includes('coming') || | |
| panel.querySelector('.fa-circle-xmark') | |
| ) { | |
| const reason = | |
| panel.innerText.match(/(sold out|out of stock|coming.*)/i)?.[0] ?? | |
| 'Unavailable'; | |
| return { available: false, reason }; | |
| } | |
| if ( | |
| panel.querySelector('.fa-circle-check') || | |
| text.includes('in stock') || | |
| text.includes('limited availability') | |
| ) { | |
| return { available: true }; | |
| } | |
| return { available: false, reason: 'Unknown status' }; | |
| } | |
| /* ------------------------------ | |
| * Google Maps URL builder | |
| * ------------------------------ */ | |
| function buildGoogleMapsUrl(stores) { | |
| // Convert "CO - Denver" → "Denver, CO" | |
| const locations = stores.map(s => { | |
| const parts = s.name.split('-').map(p => p.trim()); | |
| return parts.length === 2 | |
| ? `${parts[1]}, ${parts[0]}` | |
| : s.name; | |
| }); | |
| return ( | |
| 'https://www.google.com/maps/search/?api=1&query=' + | |
| encodeURIComponent(locations.join(' OR ')) | |
| ); | |
| } | |
| /* ------------------------------ | |
| * UI: Popup | |
| * ------------------------------ */ | |
| function showPopup(results) { | |
| const overlay = document.createElement('div'); | |
| overlay.style.cssText = ` | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0,0,0,.6); | |
| z-index: 99999; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| `; | |
| const box = document.createElement('div'); | |
| box.style.cssText = ` | |
| background: #fff; | |
| padding: 20px; | |
| width: 520px; | |
| max-height: 75vh; | |
| overflow: auto; | |
| border-radius: 8px; | |
| font-family: sans-serif; | |
| `; | |
| const inStock = results.filter(r => r.available); | |
| const outStock = results.filter(r => !r.available); | |
| const mapsUrl = | |
| inStock.length > 0 ? buildGoogleMapsUrl(inStock) : null; | |
| box.innerHTML = ` | |
| <h2>In-stock locations (${inStock.length})</h2> | |
| <ul> | |
| ${ | |
| inStock.length | |
| ? inStock | |
| .map( | |
| r => | |
| `<li>✅ <a href="${r.url}" target="_blank" rel="noopener">${r.name}</a></li>` | |
| ) | |
| .join('') | |
| : '<li>None</li>' | |
| } | |
| </ul> | |
| ${ | |
| mapsUrl | |
| ? `<a href="${mapsUrl}" target="_blank" rel="noopener" | |
| style="display:inline-block; margin-top:10px; font-weight:600;"> | |
| 🗺 Open in Google Maps | |
| </a>` | |
| : '' | |
| } | |
| <details style="margin-top:15px"> | |
| <summary>Out of stock (${outStock.length})</summary> | |
| <ul> | |
| ${outStock | |
| .map( | |
| r => | |
| `<li>❌ <a href="${r.url}" target="_blank" rel="noopener">${r.name}</a> — ${r.reason}</li>` | |
| ) | |
| .join('')} | |
| </ul> | |
| </details> | |
| <button style="margin-top:15px">Close</button> | |
| `; | |
| box.querySelector('button:last-of-type').onclick = () => overlay.remove(); | |
| overlay.appendChild(box); | |
| document.body.appendChild(overlay); | |
| } | |
| /* ------------------------------ | |
| * Store Discovery | |
| * ------------------------------ */ | |
| function getStores() { | |
| return [...document.querySelectorAll('.dropdown-itemLI a')] | |
| .map(a => ({ | |
| name: a.innerText.trim(), | |
| url: new URL(a.getAttribute('href'), location.origin).href | |
| })) | |
| .filter(s => !s.name.includes('Shippable')); | |
| } | |
| function fetchStore(store) { | |
| return new Promise(resolve => { | |
| GM_xmlhttpRequest({ | |
| method: 'GET', | |
| url: store.url, | |
| onload: res => { | |
| const status = parseStockFromHTML(res.responseText); | |
| resolve({ ...store, ...status }); | |
| }, | |
| onerror: () => | |
| resolve({ ...store, available: false, reason: 'Request failed' }) | |
| }); | |
| }); | |
| } | |
| async function checkStoresParallel(stores, btn) { | |
| const concurrency = 5; | |
| const results = []; | |
| let index = 0; | |
| async function worker() { | |
| while (index < stores.length) { | |
| const current = stores[index++]; | |
| btn.textContent = `Checking ${results.length + 1}/${stores.length}…`; | |
| results.push(await fetchStore(current)); | |
| await sleep(300); | |
| } | |
| } | |
| await Promise.all(Array.from({ length: concurrency }, worker)); | |
| return results; | |
| } | |
| /* ------------------------------ | |
| * UI Injection | |
| * ------------------------------ */ | |
| function injectButton() { | |
| const btn = document.createElement('button'); | |
| btn.id = 'mcFindStockBtn'; | |
| btn.textContent = 'Find in-stock locations'; | |
| btn.style.cssText = ` | |
| margin-top: 10px; | |
| padding: 10px 14px; | |
| font-size: 14px; | |
| background: #0071ce; | |
| color: #fff; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| `; | |
| btn.onclick = async () => { | |
| btn.disabled = true; | |
| const stores = getStores(); | |
| const results = await checkStoresParallel(stores, btn); | |
| btn.disabled = false; | |
| btn.textContent = 'Find in-stock locations'; | |
| showPopup(results); | |
| }; | |
| document.querySelector('.changeMyStore')?.appendChild(btn); | |
| } | |
| /* ------------------------------ | |
| * Init | |
| * ------------------------------ */ | |
| const observer = new MutationObserver(() => { | |
| if (document.querySelector('.dropdown-itemLI')) { | |
| observer.disconnect(); | |
| injectButton(); | |
| } | |
| }); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment