// Content script: injects an "An Bewerbungs-Tracker senden" button next to the
// Indeed job description, scrapes the job details and forwards them to the
// tracker's import API (via the background service worker).
(function () {
'use strict';
const BTN_ID = 'bt-import-container';
// ----- Scraping ---------------------------------------------------------
function cleanText(el) {
if (!el) return '';
return (el.innerText || el.textContent || '').replace(/\s+\n/g, '\n').trim();
}
// The visible detail pane (right side on search pages, whole page on /viewjob).
function getDetailRoot() {
return (
document.querySelector('.jobsearch-JobComponent') ||
document.querySelector('#jobsearch-ViewjobPaneWrapper') ||
document.querySelector('.jobsearch-RightPane') ||
document
);
}
function getTitle(root) {
const el =
root.querySelector('[data-testid="jobsearch-JobInfoHeader-title"]') ||
root.querySelector('.jobsearch-JobInfoHeader-title');
if (!el) return '';
// Drop the trailing "- job post" / "- Stellenanzeige" label Indeed appends.
let text = cleanText(el);
text = text.replace(/\s*-\s*(job post|Stellenanzeige)\s*$/i, '').trim();
return text;
}
function getCompany(root) {
const container =
root.querySelector('[data-testid="jobsearch-CompanyInfoContainer"]') || root;
const el =
container.querySelector('[data-testid="inlineHeader-companyName"]') ||
container.querySelector('[data-company-name="true"]') ||
container.querySelector('[data-testid="company-name"]');
return cleanText(el);
}
function getLocation(root) {
const el =
root.querySelector('[data-testid="jobsearch-JobInfoHeader-companyLocation"]') ||
root.querySelector('[data-testid="inlineHeader-companyLocation"]');
return cleanText(el);
}
function getSalary(root) {
const el =
root.querySelector('#salaryInfoAndJobType') ||
root.querySelector('[data-testid="jobsearch-OtherJobDetailsContainer"]');
return cleanText(el);
}
function getDescription(root) {
const el = root.querySelector('#jobDescriptionText');
return cleanText(el);
}
// Build a stable job URL. On the search page the selected job key lives in the
// `vjk` query param; a clean /viewjob URL is nicer than the search URL.
function getSourceUrl() {
try {
const u = new URL(window.location.href);
const vjk = u.searchParams.get('vjk') || u.searchParams.get('jk');
if (vjk) return `${u.origin}/viewjob?jk=${vjk}`;
} catch (_) { /* ignore */ }
return window.location.href;
}
function scrapeJob() {
const root = getDetailRoot();
return {
firma: getCompany(root),
stelle: getTitle(root),
ort: getLocation(root),
gehalt: getSalary(root),
stellenbeschreibung: getDescription(root),
quelle_url: getSourceUrl(),
art: 'Indeed',
};
}
// ----- Button UI --------------------------------------------------------
const ICON_SEND =
'';
function buildContainer() {
const wrap = document.createElement('div');
wrap.className = 'bt-import-wrap';
wrap.id = BTN_ID;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'bt-import-btn';
btn.innerHTML = ICON_SEND + 'An Bewerbungs-Tracker senden';
const status = document.createElement('div');
status.className = 'bt-import-status';
btn.addEventListener('click', () => onClick(btn, status));
wrap.appendChild(btn);
wrap.appendChild(status);
return wrap;
}
function setStatus(status, html, cls) {
status.className = 'bt-import-status' + (cls ? ' ' + cls : '');
status.innerHTML = html;
}
function duplicateWarning(matches) {
const lines = (matches || []).slice(0, 5).map(function (m) {
const parts = [m.firma, m.stelle].filter(Boolean).join(' — ');
const meta = [m.datum, m.status].filter(Boolean).join(', ');
return '• ' + parts + (meta ? ' (' + meta + ')' : '');
});
return 'Für diese Stelle scheint bereits eine Bewerbung zu existieren:\n\n' +
lines.join('\n') + '\n\nTrotzdem als weitere Bewerbung importieren?';
}
function onClick(btn, status) {
const job = scrapeJob();
if (!job.firma || !job.stelle) {
setStatus(status, 'Konnte Firma oder Stelle nicht auslesen. Bitte eine Stelle öffnen.', 'bt-err');
return;
}
const label = btn.querySelector('span');
const originalLabel = label ? label.textContent : '';
const send = (payload) => {
btn.disabled = true;
btn.innerHTML = 'Wird gesendet…';
setStatus(status, '', '');
chrome.runtime.sendMessage({ type: 'IMPORT_JOB', payload: payload }, (resp) => {
btn.innerHTML = ICON_SEND + '' + (originalLabel || 'An Bewerbungs-Tracker senden') + '';
if (chrome.runtime.lastError) {
btn.disabled = false;
setStatus(status, 'Fehler: ' + chrome.runtime.lastError.message, 'bt-err');
return;
}
if (resp && resp.duplicate) {
btn.disabled = false;
if (confirm(duplicateWarning(resp.matches))) {
send(Object.assign({}, payload, { force: true }));
} else {
setStatus(status, 'Import abgebrochen (mögliches Duplikat).', '');
}
return;
}
if (!resp || !resp.ok) {
btn.disabled = false;
setStatus(status, (resp && resp.error) || 'Unbekannter Fehler.', 'bt-err');
return;
}
const link = resp.openUrl
? ` Entwurf öffnen`
: '';
setStatus(
status,
'✓ ' + (resp.message || 'Als Entwurf angelegt. Unterlagen werden erstellt.') + link,
'bt-ok'
);
setTimeout(() => { btn.disabled = false; }, 1500);
});
};
send(job);
}
// ----- Injection + SPA handling -----------------------------------------
// Insert the button just above the job description text, so it sits right
// next to the "Vollständige Stellenbeschreibung" heading.
function findAnchor() {
const heading = document.querySelector('#jobDescriptionTitleHeading');
if (heading) return { node: heading, position: 'after' };
const desc = document.querySelector('#jobDescriptionText');
if (desc) return { node: desc, position: 'before' };
const titleContainer = document.querySelector('.jobsearch-JobInfoHeader-title-container');
if (titleContainer) return { node: titleContainer, position: 'after' };
return null;
}
function inject() {
const anchor = findAnchor();
if (!anchor) return;
const existing = document.getElementById(BTN_ID);
// Re-inject if missing or detached from the current anchor's parent.
if (existing && existing.parentNode === anchor.node.parentNode) return;
if (existing) existing.remove();
const container = buildContainer();
if (anchor.position === 'after') {
anchor.node.parentNode.insertBefore(container, anchor.node.nextSibling);
} else {
anchor.node.parentNode.insertBefore(container, anchor.node);
}
}
// Indeed is a single-page app: the detail pane swaps without a full reload.
// Observe DOM changes and (debounced) re-inject / keep the button in place.
let debounce = null;
const observer = new MutationObserver(() => {
if (debounce) clearTimeout(debounce);
debounce = setTimeout(inject, 300);
});
function start() {
inject();
observer.observe(document.body, { childList: true, subtree: true });
}
if (document.body) {
start();
} else {
document.addEventListener('DOMContentLoaded', start);
}
})();