Skip to content

Instantly share code, notes, and snippets.

@SmugZombie
Created August 24, 2025 05:57
Show Gist options
  • Save SmugZombie/fee71c0cb3b31465805394d0cebffa0f to your computer and use it in GitHub Desktop.
Save SmugZombie/fee71c0cb3b31465805394d0cebffa0f to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Next Button (TJX)
// @namespace http://tampermonkey.net/
// @version 2025-08-23
// @description Adds a button to navigate to the next product on TJX sites
// @author You
// @match https://tjmaxx.tjx.com/*
// @match https://www.marshalls.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=tjx.com
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const BTN_ID = 'tjx-next-product-btn';
function getNextProductUrl(href = window.location.href) {
const url = new URL(href);
// url.pathname doesn't include query, so no need to split on "?"
const parts = url.pathname.split('/').filter(Boolean);
const last = parts[parts.length - 1];
// Product pages look like .../<PRODUCT_ID>
const id = parseInt(last, 10);
if (!Number.isNaN(id)) {
parts[parts.length - 1] = String(id + 1);
url.pathname = '/' + parts.join('/');
return url.toString();
}
return null;
}
function ensureButton() {
// Avoid adding on non-product pages
const nextUrl = getNextProductUrl();
const existing = document.getElementById(BTN_ID);
if (!nextUrl) {
if (existing) existing.remove();
return;
}
if (existing) {
existing.onclick = () => (window.location.href = nextUrl);
return;
}
// Make sure body exists
if (!document.body) {
requestAnimationFrame(ensureButton);
return;
}
const button = document.createElement('button');
button.id = BTN_ID;
button.textContent = 'Next Product';
Object.assign(button.style, {
position: 'fixed',
top: '10px',
right: '10px',
padding: '10px 20px',
backgroundColor: '#007BFF',
color: '#FFF',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
zIndex: '999999',
fontSize: '16px',
});
button.onclick = () => (window.location.href = nextUrl);
document.body.appendChild(button);
}
// Run immediately (document-idle) and also on SPA route changes
function hookHistory() {
const _push = history.pushState;
const _replace = history.replaceState;
history.pushState = function () {
const ret = _push.apply(this, arguments);
queueMicrotask(ensureButton);
return ret;
};
history.replaceState = function () {
const ret = _replace.apply(this, arguments);
queueMicrotask(ensureButton);
return ret;
};
window.addEventListener('popstate', () => queueMicrotask(ensureButton));
}
// Initial run + SPA hooks
ensureButton();
hookHistory();
// In case the site lazily mounts content, do a one-time short retry burst
let retries = 10;
const iv = setInterval(() => {
ensureButton();
if (--retries <= 0) clearInterval(iv);
}, 300);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment