function esc(str) { return (str || '').toString() .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } // Entry-level / hourly-wage filtering: a rough but effective heuristic for // first-time workers who don't want to wade through senior/salaried postings. const ENTRY_LEVEL_EXCLUDE_KEYWORDS = [ 'senior', 'sr.', 'sr ', 'lead ', 'principal', 'staff ', 'director', 'manager', 'management', 'vp ', 'vice president', 'head of', 'architect', 'chief', 'executive', 'president', 'supervisor', 'experienced', ]; const ENTRY_LEVEL_LEVEL_SUFFIX = /\b(ii|iii|iv|v)\b\s*$/i; function isEntryLevelTitle(title) { const t = (title || '').toLowerCase(); if (ENTRY_LEVEL_LEVEL_SUFFIX.test(t)) return false; return !ENTRY_LEVEL_EXCLUDE_KEYWORDS.some((kw) => t.includes(kw)); } // Gig/rideshare/delivery-app postings tend to flood broad, keyword-less // searches. These are 1099 contract platforms, not the W2 employer jobs // someone building a first work history is generally after. const GIG_APP_KEYWORDS = [ 'uber', 'lyft', 'doordash', 'door dash', 'instacart', 'grubhub', 'postmates', 'shipt', 'amazon flex', 'spark driver', 'roadie', 'gopuff', 'go puff', 'favor delivery', 'taskrabbit', 'wonolo', 'curri', 'dolly', 'veho', ]; function isGigAppJob(job) { const haystack = `${job.title} ${job.company}`.toLowerCase(); return GIG_APP_KEYWORDS.some((kw) => haystack.includes(kw)); } function normalize(source, title, company, location, url, salary, posted) { return { source, title: title || 'Untitled role', company: company || 'Unknown company', location: location || '', url, salary: salary || '', posted: posted || '', }; } async function searchRemotive(query) { const params = new URLSearchParams(query ? { search: query } : {}); const url = `https://remotive.com/api/remote-jobs?${params.toString()}`; try { const res = await fetch(url); if (!res.ok) return []; const data = await res.json(); return (data.jobs || []).slice(0, 20).map((job) => normalize( 'Remotive', job.title, job.company_name, job.candidate_required_location || 'Remote', job.url, job.salary || '', job.publication_date ? job.publication_date.slice(0, 10) : '' ) ); } catch { return []; } } async function searchMuse(query, location) { const params = new URLSearchParams({ page: '0' }); if (query) params.set('q', query); const url = `https://www.themuse.com/api/public/jobs?${params.toString()}`; try { const res = await fetch(url); if (!res.ok) return []; const data = await res.json(); let results = (data.results || []).map((job) => normalize( 'The Muse', job.name, job.company && job.company.name, job.locations && job.locations.length ? job.locations.map((l) => l.name).join(', ') : '', `https://www.themuse.com/jobs/${job.company ? job.company.short_name : ''}/${job.short_name || ''}`, '', job.publication_date ? job.publication_date.slice(0, 10) : '' ) ); if (location) { const loc = location.toLowerCase(); results = results.filter( (r) => !r.location || r.location.toLowerCase().includes(loc) || r.location.toLowerCase().includes('flexible') ); } return results.slice(0, 20); } catch { return []; } } async function searchJobs(query, location, entryLevel, hideGigApps) { const [remotive, muse] = await Promise.all([ searchRemotive(query), searchMuse(query, location), ]); let all = [...remotive, ...muse]; if (entryLevel) { all = all.filter((job) => isEntryLevelTitle(job.title)); } if (hideGigApps) { all = all.filter((job) => !isGigAppJob(job)); } const seen = new Set(); return all.filter((job) => { const key = `${job.title.toLowerCase()}|${job.company.toLowerCase()}`; if (seen.has(key)) return false; seen.add(key); return true; }); } document.getElementById('job-search-form').addEventListener('submit', async (e) => { e.preventDefault(); const q = document.getElementById('q').value.trim(); const location = document.getElementById('location').value.trim(); const entryLevel = document.getElementById('entry-level').checked; const hideGigApps = document.getElementById('hide-gig-apps').checked; const statusEl = document.getElementById('status'); const resultsEl = document.getElementById('results'); statusEl.textContent = 'Searching...'; resultsEl.innerHTML = ''; try { const results = await searchJobs(q, location, entryLevel, hideGigApps); if (results.length === 0) { const forQuery = q ? ` for "${q}"` : ''; const suggestions = [ entryLevel && 'uncheck "entry-level & hourly-wage jobs only"', hideGigApps && 'uncheck "hide gig/delivery apps"', 'try a different keyword', 'leave location blank', ].filter(Boolean).join(', '); statusEl.textContent = `No results${forQuery}. Try: ${suggestions}.`; return; } statusEl.textContent = `${results.length} result${results.length === 1 ? '' : 's'} found`; resultsEl.innerHTML = results.map((job) => `