Initial import of hire-me.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
delikesance
2026-09-04 20:08:12 +00:00
co-authored by Cursor
commit 9c734470b3
23 changed files with 2463 additions and 0 deletions
+261
View File
@@ -0,0 +1,261 @@
const statusEl = document.querySelector("#status");
const listEl = document.querySelector("#list");
const metaEl = document.querySelector("#meta");
const dialog = document.querySelector("#preview");
const previewTitle = document.querySelector("#preview-title");
const previewFrom = document.querySelector("#preview-from");
const previewReply = document.querySelector("#preview-reply");
const previewAttach = document.querySelector("#preview-attach");
const previewTo = document.querySelector("#preview-to");
const previewSubject = document.querySelector("#preview-subject");
const previewText = document.querySelector("#preview-text");
const previewError = document.querySelector("#preview-error");
const previewSend = document.querySelector("#preview-send");
const previewRegen = document.querySelector("#preview-regen");
/** @type {{ offer: any, article: HTMLElement, button: HTMLButtonElement, input: HTMLInputElement, hint: HTMLElement | null, feedback: HTMLElement, language: string } | null} */
let active = null;
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function sourceLabel(source) {
if (source === "website") return "trouvé sur le site";
if (source === "gemini") return "détecté via Gemini";
if (source === "guess") return "estimé (careers/recruiting@)";
if (source === "description") return "dans loffre";
if (source === "manual") return "saisi";
if (source === "cache") return "en cache";
return source || "recherche…";
}
function showPreviewError(message) {
if (!previewError) return;
previewError.hidden = !message;
previewError.textContent = message || "";
}
async function loadPreview(forceTo) {
if (!active) return;
const { offer, button, input, feedback } = active;
showPreviewError("");
button.disabled = true;
button.textContent = "Rédaction…";
feedback.hidden = false;
feedback.className = "feedback";
feedback.textContent = "Gemini rédige le mail…";
if (previewRegen) previewRegen.disabled = true;
if (previewSend) previewSend.disabled = true;
try {
const response = await fetch("/api/preview", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: offer.id,
to: forceTo || input.value.trim() || undefined,
}),
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || "Impossible de préparer le mail");
active.language = data.language || "en";
previewTitle.textContent = `${data.company}${data.title}`;
previewFrom.textContent = data.from;
previewReply.textContent = data.replyTo;
previewAttach.textContent = data.attachment;
previewTo.value = data.to;
previewSubject.value = data.subject;
previewText.value = data.text;
input.value = data.to;
if (active.hint) active.hint.textContent = sourceLabel(data.emailSource);
feedback.hidden = true;
dialog?.showModal();
} catch (error) {
feedback.className = "feedback err";
feedback.textContent = error instanceof Error ? error.message : "Erreur";
} finally {
button.disabled = false;
button.textContent = "Postuler";
if (previewRegen) previewRegen.disabled = false;
if (previewSend) previewSend.disabled = false;
}
}
async function sendPreview() {
if (!active) return;
const { offer, article, button, input, hint, feedback } = active;
const to = previewTo.value.trim();
const subject = previewSubject.value.trim();
const text = previewText.value.trim();
showPreviewError("");
if (!to || !subject || !text) {
showPreviewError("Destinataire, objet et corps sont requis.");
return;
}
previewSend.disabled = true;
previewRegen.disabled = true;
previewSend.textContent = "Envoi…";
try {
const response = await fetch("/api/apply", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: offer.id,
to,
subject,
text,
language: active.language,
}),
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || "Échec de candidature");
article.classList.add("applied");
button.classList.add("done");
button.textContent = "Envoyé";
button.disabled = true;
input.value = data.to;
input.disabled = true;
if (hint) hint.textContent = sourceLabel(data.emailSource);
feedback.hidden = false;
feedback.className = "feedback ok";
feedback.textContent = `Envoyé à ${data.to}${data.subject}`;
dialog?.close();
} catch (error) {
showPreviewError(error instanceof Error ? error.message : "Erreur");
} finally {
previewSend.disabled = false;
previewRegen.disabled = false;
previewSend.textContent = "Envoyer";
}
}
function renderOffer(offer) {
const applied = Boolean(offer.appliedAt);
const email = offer.emails[0] ?? "";
const article = document.createElement("article");
article.className = `offer${applied ? " applied" : ""}`;
article.dataset.id = offer.id;
article.innerHTML = `
<div class="offer-head">
<div>
<p class="company">${escapeHtml(offer.company)}</p>
<h2 class="title">${escapeHtml(offer.title)}</h2>
</div>
<span class="score">score ${escapeHtml(offer.score)}</span>
</div>
<p class="meta-row">
<span>${escapeHtml(offer.location)}</span>
<span>·</span>
<span>${escapeHtml(offer.source)}</span>
<span>·</span>
<a href="${escapeHtml(offer.url)}" target="_blank" rel="noreferrer">voir loffre</a>
</p>
<ul class="reasons">
${offer.reasons.map((reason) => `<li>${escapeHtml(reason)}</li>`).join("")}
</ul>
<p class="excerpt">${escapeHtml(offer.excerpt)}</p>
<div class="actions">
<label>
Email destinataire
<input
type="email"
name="to"
value="${escapeHtml(email)}"
placeholder="${email ? "" : "Recherche auto en cours…"}"
${applied ? "disabled" : ""}
/>
<span class="email-hint">${
email
? escapeHtml(sourceLabel(offer.emailSource))
: "sera récupéré automatiquement au clic si besoin"
}</span>
</label>
<button type="button" ${applied ? "disabled" : ""}>
${applied ? "Déjà postulé" : "Postuler"}
</button>
</div>
<p class="feedback" hidden></p>
`;
const button = article.querySelector("button");
const input = article.querySelector('input[name="to"]');
const hint = article.querySelector(".email-hint");
const feedback = article.querySelector(".feedback");
button?.addEventListener("click", async () => {
if (!button || !input || !feedback) return;
active = { offer, article, button, input, hint, feedback, language: "en" };
await loadPreview(input.value.trim());
});
return article;
}
previewSend?.addEventListener("click", () => {
void sendPreview();
});
previewRegen?.addEventListener("click", () => {
void loadPreview(previewTo?.value.trim());
});
async function boot() {
try {
const response = await fetch("/api/offers");
const data = await response.json();
if (!response.ok) throw new Error(data.error || "Impossible de charger les offres");
statusEl.hidden = true;
listEl.hidden = false;
metaEl.hidden = false;
const withEmail = data.offers.filter((o) => o.emails?.length).length;
metaEl.textContent = `${data.count} offre(s) — ${withEmail} email(s) trouvés. Postuler ouvre dabord un aperçu.`;
if (!data.offers.length) {
statusEl.hidden = false;
statusEl.textContent = "Aucune offre. Lance dabord `npm run find`.";
return;
}
for (const offer of data.offers) {
listEl.append(renderOffer(offer));
}
setTimeout(async () => {
try {
const again = await fetch("/api/offers");
const next = await again.json();
if (!again.ok) return;
for (const offer of next.offers) {
const article = listEl.querySelector(`[data-id="${offer.id}"]`);
const input = article?.querySelector('input[name="to"]');
const hint = article?.querySelector(".email-hint");
if (input && !input.disabled && offer.emails?.[0] && !input.value) {
input.value = offer.emails[0];
if (hint) hint.textContent = sourceLabel(offer.emailSource);
}
}
const filled = next.offers.filter((o) => o.emails?.length).length;
metaEl.textContent = `${next.count} offre(s) — ${filled} email(s) trouvés. Postuler ouvre dabord un aperçu.`;
} catch {
// ignore
}
}, 8000);
} catch (error) {
statusEl.classList.add("error");
statusEl.textContent = error instanceof Error ? error.message : "Erreur de chargement";
}
}
boot();