Last active
September 12, 2025 18:47
-
-
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
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
| 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(); |
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
| 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