Initial import of hire-me.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+161
@@ -0,0 +1,161 @@
|
||||
import type { Match, NormalizedJob } from "./types.ts";
|
||||
|
||||
const EU_COUNTRIES = [
|
||||
"france",
|
||||
"germany",
|
||||
"spain",
|
||||
"italy",
|
||||
"portugal",
|
||||
"netherlands",
|
||||
"belgium",
|
||||
"luxembourg",
|
||||
"austria",
|
||||
"switzerland",
|
||||
"ireland",
|
||||
"united kingdom",
|
||||
"sweden",
|
||||
"norway",
|
||||
"denmark",
|
||||
"finland",
|
||||
"poland",
|
||||
"czech",
|
||||
"czechia",
|
||||
"romania",
|
||||
"hungary",
|
||||
"greece",
|
||||
"croatia",
|
||||
"slovenia",
|
||||
"slovakia",
|
||||
"estonia",
|
||||
"latvia",
|
||||
"lithuania",
|
||||
"bulgaria",
|
||||
"europe",
|
||||
"european",
|
||||
"emea",
|
||||
];
|
||||
|
||||
const AGENCY_DENY = [
|
||||
"lemon.io",
|
||||
"g2i",
|
||||
"distantjob",
|
||||
"talent sam",
|
||||
"kdci",
|
||||
"2am.tech",
|
||||
"photon",
|
||||
"turing",
|
||||
"toptal",
|
||||
"andela",
|
||||
"telus digital",
|
||||
"a.team",
|
||||
"white cloak",
|
||||
"creative chaos",
|
||||
"dacodes",
|
||||
"coalition technologies",
|
||||
"jobs for humanity",
|
||||
"jobsforlebanon",
|
||||
"consulting1x1",
|
||||
];
|
||||
|
||||
const JUNIOR = /\b(intern|internship|trainee|stage\b|apprentice|junior|entry-level|entry level|software engineer i\b)\b/i;
|
||||
const WRONG_ROLE =
|
||||
/\b(qa engineer|quality assurance|copywriter|marketing|sales|account executive|customer support|support engineer|devops|data engineer|scraping engineer|documentation engineer|ux designer|head of engineering|game developer)\b/i;
|
||||
const WRONG_STACK =
|
||||
/\b(reactjs|react\.js|\breact\b|next\.js|angular|ruby|rails|django|\.net|asp\.net|spring ?boot|\bjava\b|\bphp\b|laravel|symfony|kotlin|golang|flutter)\b/i;
|
||||
const VUE = /\bvue(?:\.?js)?\b/i;
|
||||
const NUXT = /\bnuxt(?:\.?js)?\b/i;
|
||||
|
||||
function tokens(location: string): string[] {
|
||||
const parts = location
|
||||
.toLowerCase()
|
||||
.split(/[,/;|]+/)
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
if (parts.length === 0 && location.trim()) return [location.toLowerCase().trim()];
|
||||
return parts;
|
||||
}
|
||||
|
||||
function isEuToken(token: string): boolean {
|
||||
if (token === "uk" || token === "gb" || token === "eu") return true;
|
||||
return EU_COUNTRIES.some((country) => token === country || token.startsWith(`${country} `));
|
||||
}
|
||||
|
||||
function geoEligible(job: NormalizedJob): { ok: boolean; reason?: string; bonus: number } {
|
||||
const loc = job.location.toLowerCase().trim();
|
||||
if (!loc || loc === "worldwide" || loc === "anywhere" || loc === "remote") {
|
||||
return { ok: true, bonus: 1, reason: "remote worldwide" };
|
||||
}
|
||||
|
||||
const parts = tokens(job.location);
|
||||
const euParts = parts.filter(isEuToken);
|
||||
if (euParts.length === 0) return { ok: false, bonus: 0 };
|
||||
|
||||
const france = parts.some((part) => part === "france" || part.includes("france"));
|
||||
return {
|
||||
ok: true,
|
||||
bonus: france ? 6 : 3,
|
||||
reason: france ? "France" : "Europe",
|
||||
};
|
||||
}
|
||||
|
||||
function timezoneOk(job: NormalizedJob): boolean {
|
||||
if (job.timezones.length === 0) return true;
|
||||
return job.timezones.some((tz) => tz >= 0 && tz <= 3);
|
||||
}
|
||||
|
||||
export function matchJob(job: NormalizedJob): Match | null {
|
||||
const company = job.company.toLowerCase();
|
||||
if (AGENCY_DENY.some((name) => company.includes(name))) return null;
|
||||
|
||||
const title = job.title;
|
||||
if (JUNIOR.test(title) || WRONG_ROLE.test(title)) return null;
|
||||
if (/\b(hybrid|présentiel|on-?site|short term|fluent ukrainian)\b/i.test(title)) return null;
|
||||
if (!job.remote) return null;
|
||||
if (!timezoneOk(job)) return null;
|
||||
|
||||
const geo = geoEligible(job);
|
||||
if (!geo.ok) return null;
|
||||
|
||||
const vueTitle = VUE.test(title);
|
||||
const nuxtTitle = NUXT.test(title) || /\bsidebase\b/i.test(title);
|
||||
const conflictingTitle = WRONG_STACK.test(title) && !vueTitle && !nuxtTitle;
|
||||
|
||||
if (conflictingTitle) return null;
|
||||
if (!vueTitle && !nuxtTitle) return null;
|
||||
|
||||
const body = `${job.excerpt} ${job.description}`.toLowerCase().slice(0, 4000);
|
||||
const reasons: string[] = [];
|
||||
let score = geo.bonus;
|
||||
if (geo.reason) reasons.push(geo.reason);
|
||||
|
||||
if (nuxtTitle) {
|
||||
score += 8;
|
||||
reasons.push("Nuxt");
|
||||
}
|
||||
if (vueTitle) {
|
||||
score += 7;
|
||||
reasons.push("Vue");
|
||||
}
|
||||
if (/\btypescript\b/i.test(`${title} ${job.tags.join(" ")} ${body}`)) {
|
||||
score += 2;
|
||||
reasons.push("TypeScript");
|
||||
}
|
||||
if (/\bnode(?:\.?js)?\b/i.test(`${title} ${job.tags.join(" ")} ${body}`)) {
|
||||
score += 2;
|
||||
reasons.push("Node.js");
|
||||
}
|
||||
if (/\bfull[\s-]?stack\b/i.test(title)) {
|
||||
score += 2;
|
||||
reasons.push("Fullstack");
|
||||
} else if (/\bfront[\s-]?end\b/i.test(title)) {
|
||||
score += 1;
|
||||
reasons.push("Frontend");
|
||||
}
|
||||
if (/\b(travel|booking|ndc|gds|fintech|b2b)\b/i.test(`${title} ${body}`)) {
|
||||
score += 2;
|
||||
reasons.push("domaine proche");
|
||||
}
|
||||
|
||||
if (score < 8) return null;
|
||||
return { job, score, reasons };
|
||||
}
|
||||
Reference in New Issue
Block a user