Skip to content

Instantly share code, notes, and snippets.

@lwcorp
Last active September 12, 2025 18:47
Show Gist options
  • Save lwcorp/81bbb6d5ce80b5f403d68fb38eb8d40d to your computer and use it in GitHub Desktop.
Save lwcorp/81bbb6d5ce80b5f403d68fb38eb8d40d to your computer and use it in GitHub Desktop.
Bookmarklet - Enhance various LinkedIn pages: 1) highlight only relevant notifications 2) expose job applies/views
javascript:function init(){if("linkedin.com"!==location.hostname.replace(/^www\./,""))return void alert("This script only works on LinkedIn");if(location.pathname.startsWith("/notifications/"))loadAll("section button:last-of-type","section:nth-of-type(2)>div>div>div>div","a[href*=\"/update/");else if(location.pathname.startsWith("/jobs/view/")||location.search.startsWith("?currentJobId="))initLinkedInJobStats();else return void alert("Unsupported page")}async function loadAll(a,b,c){document.documentElement.scrollHeight;let d;const e=Date.now();do{if(Date.now()-e>1e3*2){alert("Timeout reached, proceeding with available content");break}if(d=document.documentElement.scrollHeight,window.scrollTo(0,d),await new Promise(a=>setTimeout(a,1e3*2)),d===document.documentElement.scrollHeight)break}while(!0);window.scrollTo(0,0),await new Promise(a=>setTimeout(a,1)),grabText(a,b,c)}function grabText(a,b,c){let d=document.getElementById("highlighterParent");if(!d){d=document.createElement("span"),d.id="highlighterParent";const e=document.createElement("style");e.id="highlightSpecialStyles",e.textContent=".highlightSpecial { background-color: Lavender; } @media (prefers-color-scheme: dark) { .highlightSpecial { background-color: DarkSlateGray; } }",d.appendChild(e),highlighterElement=document.createElement("select"),highlighterElement.id="highlighter",highlighterElement.onchange=()=>stylize(highlighterElement,b,c);[{value:"remove",text:"Keep only what's relevant"},{value:"highlight",text:"Highlight only what's relevant"},{value:"reset",text:"Reset"},{value:"close",text:"Close this box"}].forEach(a=>{const b=document.createElement("option");b.value=a.value,b.textContent=a.text,highlighterElement.appendChild(b)}),d.appendChild(highlighterElement),document.querySelector(a).after(d),highlighterElement.onchange()}}function stylize(a,b,c){const d=["replied","mentioned you in a","commented"];let e=a.value;"close"==e&&(e="reset");const f=document.querySelectorAll(b);f.forEach(a=>{const b=a.querySelector(c);let f=!1;if(b){const c=b.textContent.toLowerCase();d.forEach(b=>{c.includes(b)&&(f=!0,"highlight"===e||"remove"===e?a.classList.add("highlightSpecial"):"reset"===e?(a.classList.remove("highlightSpecial"),a.style.display=""):void 0)})}f||("highlight"===e?a.style.display="":"remove"===e?a.style.display="none":"reset"===e?a.style.display="":void 0)}),"close"==a.value&&document.getElementById(a.id).remove()}function log(...a){{console.log("[JobStatsExt]",...a)}}function getCsrfToken(){const a=document.cookie.match(/JSESSIONID="([^"]+)"/),b=a?a[1]:"";return log("CSRF token:",b||"(none found)"),b}async function fetchStats(a,b){log("fetchStats called for jobId:",a);try{const c=await fetch(`/voyager/api/jobs/jobPostings/${a}`,{credentials:"include",headers:{accept:"application/json","csrf-token":b,"x-restli-protocol-version":"2.0.0"}});log("Fetch response status:",c.status);const d=await c.json();log("Raw JSON data:",d);const e=d.applies??d.data?.applies,f=d.views??d.data?.views;return log("Parsed applies/views:",e,f),"number"!=typeof e||"number"!=typeof f?(log("Stats are not numbers, aborting"),null):{applies:e,views:f}}catch(a){return log("Error fetching job stats:",a),null}}async function injectStats(){log("injectStats triggered, current URL path:",location.pathname);const a=location.pathname.match(/^\/jobs\/view\/(\d+)/)||location.search.match(/currentJobId=(\d+)/);if(!a)return void log("Not on a job view page, skipping");const b=a[1];log("Detected jobId:",b),await new Promise(a=>setTimeout(a,1));const c=document.querySelector("h1");if(!c)return void log("No <h1> title element found");if(c.dataset.statsInjected)return void log("Stats already injected, skipping");log("Title element found:",c),location.search.match(/currentJobId=(\d+)/)||(c.dataset.statsInjected="1");const d=getCsrfToken(),e=await fetchStats(b,d);if(!e)return void log("No stats returned, aborting injection");const{applies:f,views:g}=e;log("Injecting badge with applies/views:",f,g);let h=document.getElementById("badge_container");h&&(log("Removing existing badge"),h.remove()),h=document.createElement("span"),h.id="badge_container";const i=document.createElement("span");let j=0<g?Math.round(100*(f/g)):0;i.textContent=`${f} applies (${j}%) • ${g} views`,i.style.marginLeft="0.5em",i.style.fontSize="0.9em",i.style.color="#666",c.insertAdjacentElement("afterend",h),h.appendChild(i);const k=document.createElement("input");k.type="checkbox",k.id="monitor_stats",k.checked=!0,k.onclick=()=>initLinkedInJobStats();const l=document.createElement("label");l.htmlFor="monitor_stats",l.textContent="Monitor stats",l.style.marginLeft="0.5em",l.style.display="inline",i.insertAdjacentElement("afterend",k),k.insertAdjacentElement("afterend",l),log("Badge injected successfully")}function initLinkedInJobStats(){const a=document.getElementById("monitor_stats");if(a&&!a.checked)return void(window.statsObserver&&(window.statsObserver.disconnect(),log("Observer disconnected due to unchecked checkbox")));a||injectStats();let b=location.pathname+location.search;(window.statsObserver=new MutationObserver(()=>{const a=location.pathname+location.search;a!==b&&(b=a,log("URL or search changed, re-running injectStats"),injectStats())})).observe(document.body,{childList:!0,subtree:!0})}init();
function init() {
if (location.hostname.replace(/^www\./, '') !== 'linkedin.com') {
alert('This script only works on LinkedIn');
return;
}
if (location.pathname.startsWith('/notifications/')) {
loadAll('section button:last-of-type', 'section:nth-of-type(2)>div>div>div>div', 'a[href*="/update/');//'section:nth-of-type(2) a[href*="/update/');
} else if (location.pathname.startsWith('/jobs/view/') || location.search.startsWith('?currentJobId=')) {
initLinkedInJobStats()
} else {
alert('Unsupported page');
return;
}
}
async function loadAll(btn, parent, item) {
const initialHeight = document.documentElement.scrollHeight;
let currentHeight;
const startTime = Date.now();
const timeout = 2;
const delay = 2;
do {
if (Date.now() - startTime > timeout * 1000) {
alert('Timeout reached, proceeding with available content');
break;
}
currentHeight = document.documentElement.scrollHeight;
window.scrollTo(0, currentHeight);
await new Promise(resolve => setTimeout(resolve, delay * 1000));
if (currentHeight === document.documentElement.scrollHeight) {
break;
}
} while (true);
window.scrollTo(0, 0);
await new Promise(resolve => setTimeout(resolve, 1));
grabText(btn, parent, item);
}
function grabText(btn, parent, item) {
let highlighterParent = document.getElementById('highlighterParent');
if (!highlighterParent) {
// Create highlighterParent span
highlighterParent = document.createElement('span');
highlighterParent.id = 'highlighterParent';
// Add CSS styles with ID
const style = document.createElement('style');
style.id = 'highlightSpecialStyles';
style.textContent = '.highlightSpecial { background-color: Lavender; } \
@media (prefers-color-scheme: dark) { .highlightSpecial { background-color: DarkSlateGray; } }';
highlighterParent.appendChild(style);
highlighterElement = document.createElement('select');
highlighterElement.id = 'highlighter';
highlighterElement.onchange = () => stylize(highlighterElement, parent, item);
/*
highlighterElement.style.position = 'fixed';
highlighterElement.style.top = '10px';
highlighterElement.style.right = '10px';
highlighterElement.style.zIndex = '9999';
highlighterElement.style.padding = '5px';
highlighterElement.style.backgroundColor = 'white';
highlighterElement.style.border = '1px solid #ccc';
highlighterElement.style.borderRadius = '4px';
*/
const options = [
{ value: 'remove', text: "Keep only what's relevant" },
{ value: 'highlight', text: "Highlight only what's relevant" },
{ value: 'reset', text: 'Reset' },
{ value: 'close', text: 'Close this box' }
];
options.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option.value;
optionElement.textContent = option.text;
highlighterElement.appendChild(optionElement);
});
highlighterParent.appendChild(highlighterElement);
document.querySelector(btn).after(highlighterParent);
highlighterElement.onchange();
}
}
function stylize(elm, parent, item) {
const triggerWords = ['replied', 'mentioned you in a', 'commented'];
let theAction = elm.value;
if (theAction == 'close') {
theAction = 'reset';
}
const parentElements = document.querySelectorAll(parent);
parentElements.forEach(parentEl => {
const itemElement = parentEl.querySelector(item);
let found = false;
if (itemElement) {
const text = itemElement.textContent.toLowerCase();
triggerWords.forEach(word => {
if (text.includes(word)) {
found = true;
switch(theAction) {
case 'highlight':
case 'remove':
parentEl.classList.add('highlightSpecial');
break;
case 'reset':
parentEl.classList.remove('highlightSpecial');
parentEl.style.display = '';
break;
}
}
});
}
if (!found) {
switch(theAction) {
case 'highlight':
parentEl.style.display = '';
break;
case 'remove':
parentEl.style.display = 'none';
break;
case 'reset':
parentEl.style.display = '';
break;
}
}
});
if (elm.value == 'close') {
document.getElementById(elm.id).remove();
}
}
function log(...args) {
const logging = true;
if (logging) {
const PREFIX = '[JobStatsExt]';
console.log(PREFIX, ...args);
}
}
// 1. Grab CSRF token from the JSESSIONID cookie
function getCsrfToken() {
const match = document.cookie.match(/JSESSIONID="([^"]+)"/);
const token = match ? match[1] : '';
log('CSRF token:', token || '(none found)');
return token;
}
// 2. Fetch applies/views for a given jobId
async function fetchStats(jobId, csrfToken) {
log('fetchStats called for jobId:', jobId);
try {
const resp = await fetch(`/voyager/api/jobs/jobPostings/${jobId}`, {
credentials: 'include',
headers: {
'accept': 'application/json',
'csrf-token': csrfToken,
'x-restli-protocol-version': '2.0.0'
}
});
/* A bunch of console debugging because this is a weekend project.
A better developer than me (well, any actual developer) will probably replace this with something more robust.*/
log('Fetch response status:', resp.status);
const data = await resp.json();
log('Raw JSON data:', data);
const applies = data.applies ?? data.data?.applies;
const views = data.views ?? data.data?.views;
log('Parsed applies/views:', applies, views);
if (typeof applies !== 'number' || typeof views !== 'number') {
log('Stats are not numbers, aborting');
return null;
}
return { applies, views };
} catch (e) {
log('Error fetching job stats:', e);
return null;
}
}
// 3. Finds the <h1> title, fetches stats, and injects a badge
async function injectStats() {
log('injectStats triggered, current URL path:', location.pathname);
const match = location.pathname.match(/^\/jobs\/view\/(\d+)/) || location.search.match(/currentJobId=(\d+)/);
if (!match) {
log('Not on a job view page, skipping');
return;
}
const jobId = match[1];
log('Detected jobId:', jobId);
await new Promise(resolve => setTimeout(resolve, 1));
const titleEl = document.querySelector('h1');
if (!titleEl) {
log('No <h1> title element found');
return;
}
if (titleEl.dataset.statsInjected) {
log('Stats already injected, skipping');
return;
}
log('Title element found:', titleEl);
if (!location.search.match(/currentJobId=(\d+)/)) {
titleEl.dataset.statsInjected = '1'; // Prevent double-injection
}
const csrfToken = getCsrfToken();
const stats = await fetchStats(jobId, csrfToken);
if (!stats) {
log('No stats returned, aborting injection');
return;
}
const { applies, views } = stats;
log('Injecting badge with applies/views:', applies, views);
let badgeContainer = document.getElementById('badge_container');
if (badgeContainer) {
log('Removing existing badge');
badgeContainer.remove();
}
badgeContainer = document.createElement('span');
badgeContainer.id = 'badge_container';
const badge = document.createElement('span');
let percent = views > 0 ? Math.round((applies / views) * 100) : 0;
badge.textContent = `${applies} applies (${percent}%) • ${views} views`;
badge.style.marginLeft = '0.5em';
badge.style.fontSize = '0.9em';
badge.style.color = '#666';
titleEl.insertAdjacentElement('afterend', badgeContainer);
badgeContainer.appendChild(badge);
// Inject a form element
const monitorCheckbox = document.createElement('input');
monitorCheckbox.type = 'checkbox';
monitorCheckbox.id = 'monitor_stats';
monitorCheckbox.checked = true;
monitorCheckbox.onclick = () => initLinkedInJobStats();
const monitorLabel = document.createElement('label');
monitorLabel.htmlFor = 'monitor_stats';
monitorLabel.textContent = 'Monitor stats';
monitorLabel.style.marginLeft = '0.5em';
monitorLabel.style.display = 'inline';
badge.insertAdjacentElement('afterend', monitorCheckbox);
monitorCheckbox.insertAdjacentElement('afterend', monitorLabel);
// monitorCheckbox.insertAdjacentText('afterend', ' Monitor stats');
log('Badge injected successfully');
}
// Main function to initialize the LinkedIn job stats extension
function initLinkedInJobStats() {
// Check if checkbox exists and is unchecked - if so, disconnect and return
const monitorCheckbox = document.getElementById('monitor_stats');
if (monitorCheckbox && !monitorCheckbox.checked) {
if (window.statsObserver) {
window.statsObserver.disconnect();
log('Observer disconnected due to unchecked checkbox');
}
return;
} else if (!monitorCheckbox) {
// Initial injection on page load
injectStats();
}
// 4. Watch for client‐side SPA navigations
let lastUrl = location.pathname + location.search;
(window.statsObserver = new MutationObserver(() => {
const currentUrl = location.pathname + location.search;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
log('URL or search changed, re-running injectStats');
injectStats();
}
})).observe(document.body, { childList: true, subtree: true });
}
init();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment