diff --git a/src/fortune_intel/api.py b/src/fortune_intel/api.py index e70c18f..f507047 100644 --- a/src/fortune_intel/api.py +++ b/src/fortune_intel/api.py @@ -207,7 +207,7 @@ def jobs( sort: str = Query(default="newest", pattern="^(newest|verified)$"), status: str = Query(default="active", pattern="^(active|closed)$"), limit: int = Query(default=50, ge=1, le=50), - offset: int = Query(default=0, ge=0, le=5000), + offset: int = Query(default=0, ge=0, le=250000), ) -> dict[str, object]: filters = { "query": q, diff --git a/src/fortune_intel/web/app.js b/src/fortune_intel/web/app.js index 317d505..e017c89 100644 --- a/src/fortune_intel/web/app.js +++ b/src/fortune_intel/web/app.js @@ -96,6 +96,10 @@ async function loadStats() { document.querySelector("#stat-fetching-companies").textContent = Number( stats.companies_with_current_job_fetches, ).toLocaleString(); + document.querySelector("#coverage-live").textContent = Number( + stats.companies_with_successful_job_fetches, + ).toLocaleString(); + document.querySelector("#coverage-directory").textContent = Number(stats.companies).toLocaleString(); document.querySelector("#stat-h1b").textContent = Number(stats.h1b_employers).toLocaleString(); document.querySelector("#stat-evidence").textContent = Number(stats.jobs_with_evidence).toLocaleString(); document.querySelector("#stat-freshness").textContent = stats.last_verified_at @@ -107,7 +111,10 @@ async function loadStats() { } } +const jobPageSize = 50; let latestJobsRequest = 0; +let nextJobOffset = 0; +let currentCompany = ""; function replaceSearchUrl(company, filters) { const params = new URLSearchParams(); @@ -147,8 +154,12 @@ function loadSearchInputs() { return params.get("company") || ""; } -async function loadJobs(company = "") { +async function loadJobs(company = "", { append = false } = {}) { const requestId = ++latestJobsRequest; + if (!append) { + nextJobOffset = 0; + currentCompany = company; + } const params = new URLSearchParams(); const query = document.querySelector("#query").value.trim(); const location = document.querySelector("#location").value.trim(); @@ -157,7 +168,7 @@ async function loadJobs(company = "") { const verifiedWithin = document.querySelector("#verified-within").value; const sort = document.querySelector("#sort").value; const filters = { query, location, tier, openedWithin, verifiedWithin, sort }; - replaceSearchUrl(company, filters); + if (!append) replaceSearchUrl(company, filters); if (query) params.set("q", query); if (company) params.set("company", company); if (location) params.set("location", location); @@ -165,20 +176,34 @@ async function loadJobs(company = "") { if (openedWithin) params.set("opened_within_days", openedWithin); if (verifiedWithin) params.set("verified_within_hours", verifiedWithin); if (sort !== "newest") params.set("sort", sort); + params.set("limit", String(jobPageSize)); + params.set("offset", String(nextJobOffset)); const list = document.querySelector("#job-list"); const empty = document.querySelector("#empty-state"); + const loadMore = document.querySelector("#load-more-jobs"); const loading = element("div", "empty"); loading.append(element("span", "", "Loading current openings…")); empty.hidden = true; - list.replaceChildren(loading); + if (append) { + loadMore.disabled = true; + } else { + list.replaceChildren(loading); + } const response = await fetch(`/api/jobs?${params}`); if (!response.ok) throw new Error("Unable to load jobs"); const data = await response.json(); if (requestId !== latestJobsRequest) return; - document.querySelector("#result-count").textContent = `${data.count} role${data.count === 1 ? "" : "s"} shown`; - list.replaceChildren(...data.items.map(jobCard)); + nextJobOffset += data.count; + document.querySelector("#result-count").textContent = `${nextJobOffset} of ${data.total} role${data.total === 1 ? "" : "s"} shown`; + if (append) { + list.append(...data.items.map(jobCard)); + } else { + list.replaceChildren(...data.items.map(jobCard)); + } empty.hidden = data.items.length !== 0; + loadMore.hidden = nextJobOffset >= data.total; + loadMore.disabled = false; } document.querySelector("#search-form").addEventListener("submit", async (event) => { @@ -197,6 +222,14 @@ document.querySelector("#clear-filters").addEventListener("click", async () => { } }); +document.querySelector("#load-more-jobs").addEventListener("click", async () => { + try { + await loadJobs(currentCompany, { append: true }); + } catch (error) { + document.querySelector("#result-count").textContent = error.message; + } +}); + const initialCompany = loadSearchInputs(); Promise.all([loadStats(), loadJobs(initialCompany)]).catch((error) => { document.querySelector("#result-count").textContent = error.message; diff --git a/src/fortune_intel/web/index.html b/src/fortune_intel/web/index.html index db1cfaf..ec6b0ee 100644 --- a/src/fortune_intel/web/index.html +++ b/src/fortune_intel/web/index.html @@ -96,6 +96,10 @@
+ — companies have a successfully synchronized job source + out of — in the public directory. We report uncovered companies explicitly. +
Loading jobs…