383 lines
11 KiB
JavaScript
383 lines
11 KiB
JavaScript
// content.js - Handles job extraction, login automation, and profile
|
|
// title switching for app.andela.com
|
|
|
|
console.log('[Andela Content Script] Loaded on', window.location.href);
|
|
|
|
function isLoginPage() {
|
|
var emailInput = document.querySelector('input[type="email"]#email');
|
|
var emailLabel = document.querySelector('label[for="email"]');
|
|
var hasLoginForm = !!(emailInput && emailLabel);
|
|
var isLoginUrl = window.location.pathname.indexOf('/login') === 0;
|
|
return hasLoginForm || (isLoginUrl && !!emailInput);
|
|
}
|
|
|
|
function enterEmailAndClickLogin(email) {
|
|
try {
|
|
var emailInput = document.querySelector('input[type="email"]#email');
|
|
if (!emailInput) return false;
|
|
|
|
emailInput.focus();
|
|
emailInput.value = email;
|
|
emailInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
emailInput.dispatchEvent(new Event('change', { bubbles: true }));
|
|
emailInput.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));
|
|
|
|
setTimeout(function () {
|
|
var submitButton = document.querySelector('button[type="submit"]');
|
|
if (!submitButton) {
|
|
var buttons = document.querySelectorAll('button');
|
|
for (var i = 0; i < buttons.length; i++) {
|
|
var t = buttons[i].textContent.toLowerCase();
|
|
if (
|
|
t.indexOf('login') !== -1 ||
|
|
t.indexOf('sign in') !== -1 ||
|
|
t.indexOf('continue') !== -1 ||
|
|
t.indexOf('submit') !== -1
|
|
) {
|
|
submitButton = buttons[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (submitButton) {
|
|
submitButton.click();
|
|
} else {
|
|
var form = emailInput.closest('form');
|
|
if (form) {
|
|
if (form.requestSubmit) form.requestSubmit();
|
|
else form.submit();
|
|
}
|
|
}
|
|
}, 500);
|
|
|
|
return true;
|
|
} catch (err) {
|
|
console.error('[Andela Content Script] Error entering email:', err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// A section counts as "opportunities" if its heading contains this word —
|
|
// matches "Open Full-Stack Engineer opportunities", "Open AI Engineer
|
|
// opportunities", etc. Anything else (e.g. "Your applications") is
|
|
// treated as already-applied.
|
|
function isOpportunitySection(headingText) {
|
|
return /opportunit/i.test(headingText || '');
|
|
}
|
|
|
|
function getPageSections() {
|
|
var elements = document.querySelectorAll(
|
|
'h3, button, a[href*="/job-postings/"]',
|
|
);
|
|
var currentHeading = '';
|
|
var seeMoreButtons = [];
|
|
var jobLinks = [];
|
|
|
|
elements.forEach(function (el) {
|
|
if (el.tagName === 'H3') {
|
|
currentHeading = el.textContent.trim();
|
|
console.log(
|
|
'[Andela Content Script] Section heading found:',
|
|
currentHeading,
|
|
);
|
|
return;
|
|
}
|
|
if (el.tagName === 'BUTTON') {
|
|
var text = (el.textContent || '').trim().toLowerCase();
|
|
if (text.indexOf('see') === 0 && text.indexOf('more') !== -1) {
|
|
seeMoreButtons.push({ button: el, heading: currentHeading });
|
|
}
|
|
return;
|
|
}
|
|
if (el.tagName === 'A') {
|
|
jobLinks.push({ link: el, heading: currentHeading });
|
|
}
|
|
});
|
|
|
|
return { seeMoreButtons: seeMoreButtons, jobLinks: jobLinks };
|
|
}
|
|
|
|
function expandAllSeeMoreButtons(maxRounds) {
|
|
maxRounds = maxRounds || 25;
|
|
return new Promise(function (resolve) {
|
|
var rounds = 0;
|
|
|
|
function findSeeMoreButton() {
|
|
var sections = getPageSections();
|
|
for (var i = 0; i < sections.seeMoreButtons.length; i++) {
|
|
var entry = sections.seeMoreButtons[i];
|
|
if (isOpportunitySection(entry.heading)) return entry.button;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function clickNext() {
|
|
rounds++;
|
|
var btn = findSeeMoreButton();
|
|
if (!btn || rounds > maxRounds) {
|
|
resolve();
|
|
return;
|
|
}
|
|
btn.click();
|
|
setTimeout(clickNext, 700);
|
|
}
|
|
|
|
clickNext();
|
|
});
|
|
}
|
|
|
|
function extractJobFromLink(link) {
|
|
var href = link.getAttribute('href') || '';
|
|
var idMatch = href.match(/\/job-postings\/(\d+)/);
|
|
var id = idMatch ? idMatch[1] : href;
|
|
|
|
var card =
|
|
link.closest('div.rounded-lg.border.border-neutral-200.bg-white') ||
|
|
link.parentElement;
|
|
|
|
var title = '';
|
|
var company = '';
|
|
var titleEl = card
|
|
? card.querySelector('.text-primary-900.font-semibold')
|
|
: null;
|
|
|
|
if (titleEl) {
|
|
var clone = titleEl.cloneNode(true);
|
|
var companySpan = clone.querySelector('span.font-normal');
|
|
if (companySpan) {
|
|
company = companySpan.textContent.replace(/^[\s—-]+/, '').trim();
|
|
companySpan.remove();
|
|
}
|
|
title = clone.textContent.trim();
|
|
}
|
|
|
|
var paragraphs = card ? card.querySelectorAll('p') : [];
|
|
var startInfo = paragraphs[0] ? paragraphs[0].textContent.trim() : '';
|
|
var locationInfo = paragraphs[1] ? paragraphs[1].textContent.trim() : '';
|
|
|
|
var skillEls = card
|
|
? card.querySelectorAll('.flex.flex-wrap.gap-2 span')
|
|
: [];
|
|
var skills = Array.prototype.map.call(skillEls, function (s) {
|
|
return s.textContent.trim();
|
|
});
|
|
|
|
var url = href;
|
|
if (url && url.indexOf('http') !== 0) url = window.location.origin + url;
|
|
|
|
return {
|
|
id: id,
|
|
title: title,
|
|
company: company || 'Company',
|
|
posted_date: startInfo || '',
|
|
location: locationInfo || '',
|
|
skills: skills,
|
|
url: url,
|
|
};
|
|
}
|
|
|
|
function extractJobsFromPage() {
|
|
var jobs = [];
|
|
var seen = {};
|
|
var jobLinks = getPageSections().jobLinks;
|
|
|
|
jobLinks.forEach(function (entry) {
|
|
try {
|
|
var job = extractJobFromLink(entry.link);
|
|
if (!job.id || seen[job.id]) return;
|
|
seen[job.id] = true;
|
|
job.section = entry.heading;
|
|
job.kind = isOpportunitySection(entry.heading)
|
|
? 'opportunity'
|
|
: 'applied';
|
|
if (job.title) jobs.push(job);
|
|
} catch (err) {
|
|
console.error(
|
|
'[Andela Content Script] Error extracting a job card:',
|
|
err,
|
|
);
|
|
}
|
|
});
|
|
|
|
return jobs;
|
|
}
|
|
|
|
// ============================================
|
|
// Profile title switching (used to surface jobs for other roles)
|
|
// ============================================
|
|
|
|
function getCurrentProfileTitle() {
|
|
var el = document.querySelector('.text-primary-600.mt-0\\.5.text-sm');
|
|
return el ? el.textContent.trim() : '';
|
|
}
|
|
|
|
function waitFor(conditionFn, timeoutMs, intervalMs) {
|
|
timeoutMs = timeoutMs || 5000;
|
|
intervalMs = intervalMs || 200;
|
|
return new Promise(function (resolve) {
|
|
var elapsed = 0;
|
|
function check() {
|
|
var result = conditionFn();
|
|
if (result) {
|
|
resolve(result);
|
|
return;
|
|
}
|
|
elapsed += intervalMs;
|
|
if (elapsed >= timeoutMs) {
|
|
resolve(null);
|
|
return;
|
|
}
|
|
setTimeout(check, intervalMs);
|
|
}
|
|
check();
|
|
});
|
|
}
|
|
|
|
// Plain .click() doesn't reliably open this dropdown when dispatched
|
|
// programmatically — needs the full pointer-event sequence instead.
|
|
function dispatchFullClick(el) {
|
|
el.focus();
|
|
['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach(
|
|
function (type) {
|
|
el.dispatchEvent(
|
|
new MouseEvent(type, { bubbles: true, cancelable: true, view: window }),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
async function setProfessionalTitle(title) {
|
|
console.log(
|
|
'[Andela Content Script] setProfessionalTitle: looking for edit button...',
|
|
);
|
|
var editBtn = await waitFor(function () {
|
|
return document.querySelector('button[aria-label="Edit profile details"]');
|
|
}, 6000);
|
|
if (!editBtn) {
|
|
console.log('[Andela Content Script] STEP FAILED: edit button not found');
|
|
return false;
|
|
}
|
|
console.log(
|
|
'[Andela Content Script] STEP OK: edit button found, clicking...',
|
|
);
|
|
editBtn.click();
|
|
|
|
console.log(
|
|
'[Andela Content Script] looking for professional_title input...',
|
|
);
|
|
var titleInput = await waitFor(function () {
|
|
return document.querySelector('#professional_title');
|
|
}, 5000);
|
|
if (!titleInput) {
|
|
console.log(
|
|
'[Andela Content Script] STEP FAILED: professional_title input not found (modal may not have opened)',
|
|
);
|
|
return false;
|
|
}
|
|
console.log(
|
|
'[Andela Content Script] STEP OK: input found, opening dropdown...',
|
|
);
|
|
dispatchFullClick(titleInput);
|
|
|
|
console.log('[Andela Content Script] looking for option matching:', title);
|
|
var optionEl = await waitFor(
|
|
function () {
|
|
var options = document.querySelectorAll('[role="option"]');
|
|
for (var i = 0; i < options.length; i++) {
|
|
var span = options[i].querySelector('span.col-start-2') || options[i];
|
|
if (
|
|
span &&
|
|
span.textContent.trim().toLowerCase() === title.toLowerCase()
|
|
) {
|
|
return options[i];
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
5000,
|
|
400,
|
|
);
|
|
if (!optionEl) {
|
|
console.log(
|
|
'[Andela Content Script] STEP FAILED: no option matched "' + title + '"',
|
|
);
|
|
return false;
|
|
}
|
|
console.log('[Andela Content Script] STEP OK: option found, clicking...');
|
|
optionEl.click();
|
|
|
|
await new Promise(function (r) {
|
|
setTimeout(r, 400);
|
|
});
|
|
|
|
console.log('[Andela Content Script] looking for save button...');
|
|
var saveBtn = document.querySelector(
|
|
'button[type="submit"][form="edit-profile-details-form"]',
|
|
);
|
|
if (!saveBtn) {
|
|
console.log('[Andela Content Script] STEP FAILED: save button not found');
|
|
return false;
|
|
}
|
|
console.log(
|
|
'[Andela Content Script] STEP OK: save button found, clicking...',
|
|
);
|
|
saveBtn.click();
|
|
|
|
console.log('[Andela Content Script] waiting for modal to close...');
|
|
var closed = await waitFor(function () {
|
|
return !document.querySelector('#professional_title');
|
|
}, 15000);
|
|
|
|
console.log(
|
|
'[Andela Content Script] setProfessionalTitle(' + title + ') closed modal:',
|
|
!!closed,
|
|
);
|
|
return !!closed;
|
|
}
|
|
|
|
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
|
|
if (request.action === 'extractJobs') {
|
|
expandAllSeeMoreButtons().then(function () {
|
|
setTimeout(function () {
|
|
try {
|
|
sendResponse({ jobs: extractJobsFromPage() });
|
|
} catch (error) {
|
|
console.error('[Andela Content Script] Error:', error);
|
|
sendResponse({ jobs: [] });
|
|
}
|
|
}, 500);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (request.action === 'checkLoginPage') {
|
|
sendResponse({ isLoginPage: isLoginPage() });
|
|
return true;
|
|
}
|
|
|
|
if (request.action === 'enterEmailAndLogin') {
|
|
sendResponse({ success: enterEmailAndClickLogin(request.email) });
|
|
return true;
|
|
}
|
|
|
|
if (request.action === 'extractPostedDate') {
|
|
var el = document.querySelector('span.text-success-500');
|
|
sendResponse({ postedDate: el ? el.textContent.trim() : '' });
|
|
return true;
|
|
}
|
|
|
|
if (request.action === 'getCurrentProfileTitle') {
|
|
sendResponse({ title: getCurrentProfileTitle() });
|
|
return true;
|
|
}
|
|
|
|
if (request.action === 'setProfessionalTitle') {
|
|
setProfessionalTitle(request.title).then(function (success) {
|
|
sendResponse({ success: success });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
return true;
|
|
});
|