Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# AI Resume Analyzer - Job Scraper Browser Extension

This lightweight Manifest V3 browser extension allows you to scrape job titles and description bodies directly from sites like **LinkedIn** and **Indeed** and launch the AI Resume Analyzer web application with a single click.

## Features
- **Scrape Job Postings**: Automatically extracts the job role and detailed description from supported sites.
- **Context Menu Integration**: Right-click anywhere on a job posting page and choose **"Analyze Job Posting with AI Resume Analyzer"** to launch the analysis.
- **One-Click Extension Popup**: Quick access button from your toolbar.
- **Default Resume Support**: Pre-loads your default resume from the web application's local storage automatically.

---

## Installation Instructions

1. Open your browser and go to the Extensions page:
- **Chrome**: `chrome://extensions/`
- **Edge**: `edge://extensions/`
- **Brave**: `brave://extensions/`
2. Enable **Developer mode** (typically a toggle switch in the upper-right corner).
3. Click the **Load unpacked** button in the top-left.
4. Select the `extension/` folder inside this repository root.
5. Pin the extension to your toolbar for quick access!

---

## How to Set a Default Resume in the Web App

To ensure the extension can preload a resume:
1. Open the web app (e.g. `http://localhost:5173/`).
2. Go to the **Profile** / **Settings** page (click your username in the top-right navbar).
3. Under the **Default Resume** section, upload or select your primary resume, and click **Save as Default**.
4. When launched from the extension, this resume will be pre-loaded and selected automatically!
47 changes: 47 additions & 0 deletions extension/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Context menu option
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: "analyze-job",
title: "Analyze Job Posting with AI Resume Analyzer",
contexts: ["all"]
});
});

chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === "analyze-job" && tab && tab.id) {
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ["content.js"]
}, (results) => {
if (chrome.runtime.lastError || !results || !results[0]) {
console.error("Scraping failed:", chrome.runtime.lastError);
return;
}
const data = results[0].result;
openApp(data);
});
}
});

// Listener for message from popup script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "analyze") {
openApp(message.data);
sendResponse({ success: true });
}
});

function openApp(data) {
const baseUrl = "http://localhost:5173/";
const queryParams = new URLSearchParams();

if (data.role) {
queryParams.append("role", data.role.trim());
}
if (data.job_description) {
queryParams.append("job_description", data.job_description.trim());
}

const targetUrl = `${baseUrl}?${queryParams.toString()}`;
chrome.tabs.create({ url: targetUrl });
}
76 changes: 76 additions & 0 deletions extension/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
(() => {
// Scraper selectors
const selectors = {
linkedin: {
role: [
".job-details-jobs-unified-top-card__job-title",
".jobs-unified-top-card__job-title",
"h1.t-24",
"h1"
],
description: [
"#job-details",
".jobs-description-content__text",
".jobs-description__content",
".jobs-box__html-content"
]
},
indeed: {
role: [
".jobsearch-JobInfoHeader-title",
"h1.jobsearch-JobInfoHeader-title",
"h1"
],
description: [
"#jobDescriptionText",
".jobsearch-jobDescriptionText"
]
}
};

const getElementText = (selectorList) => {
for (const selector of selectorList) {
const el = document.querySelector(selector);
if (el && el.innerText.trim()) {
return el.innerText.trim();
}
}
return "";
};

const host = window.location.hostname.toLowerCase();
let role = "";
let job_description = "";

if (host.includes("linkedin.com")) {
role = getElementText(selectors.linkedin.role);
job_description = getElementText(selectors.linkedin.description);
} else if (host.includes("indeed.com")) {
role = getElementText(selectors.indeed.role);
job_description = getElementText(selectors.indeed.description);
}

// Fallbacks
if (!role) {
// Attempt general heading
const mainHeading = document.querySelector("h1");
role = mainHeading ? mainHeading.innerText.trim() : document.title.split("-")[0].trim();
}

if (!job_description) {
// Attempt to grab selected text
const selectedText = window.getSelection().toString().trim();
if (selectedText) {
job_description = selectedText;
} else {
// Fallback to body text or article content
const article = document.querySelector("article");
job_description = article ? article.innerText.trim() : document.body.innerText.trim();
}
}

return {
role: role || "Frontend Developer",
job_description: job_description || ""
};
})();
27 changes: 27 additions & 0 deletions extension/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"manifest_version": 3,
"name": "AI Resume Analyzer - Job Scraper",
"version": "1.0.0",
"description": "Scrapes job descriptions and titles to analyze against your resume in one click.",
"permissions": [
"activeTab",
"scripting",
"contextMenus"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icon.png",
"48": "icon.png",
"128": "icon.png"
}
},
"icons": {
"16": "icon.png",
"48": "icon.png",
"128": "icon.png"
}
}
84 changes: 84 additions & 0 deletions extension/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
width: 280px;
padding: 16px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #0f172a;
color: #f8fafc;
margin: 0;
}
.header {
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid #334155;
padding-bottom: 12px;
margin-bottom: 16px;
}
.logo {
font-size: 1.5rem;
}
.title {
font-size: 1.1rem;
font-weight: 700;
background: linear-gradient(135deg, #3b82f6 0%, #a855f7 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin: 0;
}
.description {
font-size: 0.85rem;
color: #94a3b8;
line-height: 1.4;
margin-bottom: 18px;
}
.btn {
width: 100%;
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
color: #ffffff;
border: none;
padding: 10px 14px;
font-size: 0.9rem;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: opacity 0.2s ease;
}
.btn:hover {
opacity: 0.9;
}
.btn:active {
transform: scale(0.98);
}
.status {
font-size: 0.8rem;
color: #22c55e;
text-align: center;
margin-top: 10px;
display: none;
}
</style>
</head>
<body>
<div class="header">
<span class="logo">🚀</span>
<h1 class="title">Resume Scraper</h1>
</div>
<p class="description">
Click the button below to scrape the job details from the current page and open them in the AI Resume Analyzer app.
</p>
<button id="scrape-btn" class="btn">
⚡ Analyze Job Posting
</button>
<div id="status" class="status">Launching Analyzer App...</div>
<script src="popup.js"></script>
</body>
</html>
50 changes: 50 additions & 0 deletions extension/popup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
document.getElementById("scrape-btn").addEventListener("click", async () => {
const statusDiv = document.getElementById("status");
statusDiv.style.display = "block";
statusDiv.innerText = "Scraping page details...";

try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab || !tab.id) {
statusDiv.innerText = "Error: Active tab not found.";
statusDiv.style.color = "#ef4444";
return;
}

// Execute the scraper content script
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ["content.js"]
}, (results) => {
if (chrome.runtime.lastError || !results || !results[0]) {
console.error("Scrape failed:", chrome.runtime.lastError);
statusDiv.innerText = "Could not scrape. Opening default app...";
statusDiv.style.color = "#fbbf24";

// Open app anyway with empty data
chrome.runtime.sendMessage({
action: "analyze",
data: { role: "", job_description: "" }
}, () => {
setTimeout(() => window.close(), 1500);
});
return;
}

const data = results[0].result;
statusDiv.innerText = "Launching Analyzer App...";
statusDiv.style.color = "#22c55e";

chrome.runtime.sendMessage({
action: "analyze",
data: data
}, () => {
setTimeout(() => window.close(), 1000);
});
});
} catch (err) {
console.error("Popup handler error:", err);
statusDiv.innerText = "Error launching app.";
statusDiv.style.color = "#ef4444";
}
});
Loading
Loading