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("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } 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 l’offre"; 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 = `

${escapeHtml(offer.company)}

${escapeHtml(offer.title)}

score ${escapeHtml(offer.score)}

${escapeHtml(offer.location)} · ${escapeHtml(offer.source)} · voir l’offre

${escapeHtml(offer.excerpt)}

`; 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 d’abord un aperçu.`; if (!data.offers.length) { statusEl.hidden = false; statusEl.textContent = "Aucune offre. Lance d’abord `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 d’abord un aperçu.`; } catch { // ignore } }, 8000); } catch (error) { statusEl.classList.add("error"); statusEl.textContent = error instanceof Error ? error.message : "Erreur de chargement"; } } boot();