// 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); } })();