diff --git a/browser-extension/README.md b/browser-extension/README.md new file mode 100644 index 0000000..3ae9c64 --- /dev/null +++ b/browser-extension/README.md @@ -0,0 +1,20 @@ +# Jobjakt Quick Capture + +The Manifest V3 extension opens the current page or selected link in Jobjakt's existing reviewed import flow. It does not scrape page content, store credentials, or submit an application automatically. + +## Install locally + +1. Open `chrome://extensions` (or the equivalent extensions page in Edge). +2. Enable developer mode. +3. Choose **Load unpacked** and select this `browser-extension` folder. +4. Open a vacancy and click the Jobjakt extension, or use **Save vacancy to Jobjakt** from the page/link context menu. + +The Jobjakt add-job dialog opens with the vacancy URL and runs the same server-side preview, validation, duplicate checking, and user review used by normal URL imports. + +## Test + +```sh +node --test browser-extension/tests/*.test.mjs +``` + +No build or package installation is required. Zip the folder contents for store submission after adding store artwork and completing the browser-store privacy declarations. diff --git a/browser-extension/background.mjs b/browser-extension/background.mjs new file mode 100644 index 0000000..a186246 --- /dev/null +++ b/browser-extension/background.mjs @@ -0,0 +1,23 @@ +import { buildCaptureUrl } from "./capture-url.mjs"; + +const menuId = "save-vacancy-to-jobjakt"; + +function openCapture(pageUrl) { + const captureUrl = buildCaptureUrl(pageUrl); + if (captureUrl) chrome.tabs.create({ url: captureUrl }); +} + +chrome.runtime.onInstalled.addListener(() => { + chrome.contextMenus.removeAll(() => { + chrome.contextMenus.create({ + id: menuId, + title: "Save vacancy to Jobjakt", + contexts: ["page", "link"] + }); + }); +}); + +chrome.action.onClicked.addListener((tab) => openCapture(tab.url)); +chrome.contextMenus.onClicked.addListener((info, tab) => { + if (info.menuItemId === menuId) openCapture(info.linkUrl || info.pageUrl || tab?.url); +}); diff --git a/browser-extension/capture-url.mjs b/browser-extension/capture-url.mjs new file mode 100644 index 0000000..a9f1ffb --- /dev/null +++ b/browser-extension/capture-url.mjs @@ -0,0 +1,15 @@ +export const JOBBJAKT_CAPTURE_ORIGIN = "https://jobs.cesnimda.uk"; + +export function buildCaptureUrl(pageUrl, origin = JOBBJAKT_CAPTURE_ORIGIN) { + let source; + try { + source = new URL(pageUrl); + } catch { + return null; + } + if (source.protocol !== "http:" && source.protocol !== "https:") return null; + + const capture = new URL("/", origin); + capture.searchParams.set("add", source.href); + return capture.href; +} diff --git a/browser-extension/manifest.json b/browser-extension/manifest.json new file mode 100644 index 0000000..f30bd9e --- /dev/null +++ b/browser-extension/manifest.json @@ -0,0 +1,14 @@ +{ + "manifest_version": 3, + "name": "Jobjakt Quick Capture", + "version": "1.0.0", + "description": "Send the current vacancy to Jobjakt's reviewed import flow.", + "permissions": ["activeTab", "contextMenus"], + "action": { + "default_title": "Save vacancy to Jobjakt" + }, + "background": { + "service_worker": "background.mjs", + "type": "module" + } +} diff --git a/browser-extension/tests/capture-url.test.mjs b/browser-extension/tests/capture-url.test.mjs new file mode 100644 index 0000000..1813cb8 --- /dev/null +++ b/browser-extension/tests/capture-url.test.mjs @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { buildCaptureUrl } from "../capture-url.mjs"; + +test("builds the existing reviewed capture route", () => { + const result = new URL(buildCaptureUrl("https://example.test/jobs/42?from=search")); + assert.equal(result.origin, "https://jobs.cesnimda.uk"); + assert.equal(result.pathname, "/"); + assert.equal(result.searchParams.get("add"), "https://example.test/jobs/42?from=search"); +}); + +test("rejects privileged and malformed URLs", () => { + assert.equal(buildCaptureUrl("chrome://settings"), null); + assert.equal(buildCaptureUrl("javascript:alert(1)"), null); + assert.equal(buildCaptureUrl("not a url"), null); +}); + +test("supports a local application origin during development", () => { + assert.equal( + buildCaptureUrl("https://arbeidsplassen.nav.no/stillinger/stilling/abc", "http://localhost:3300"), + "http://localhost:3300/?add=https%3A%2F%2Farbeidsplassen.nav.no%2Fstillinger%2Fstilling%2Fabc" + ); +});