HTML 82.1%
Python 14.6%
TypeScript 1.9%
CSS 1%
JavaScript 0.5%
1/**2 * =============================================================================3 * Job·Ka — Groupe KA4 * Auteur : Simon-Pierre Boucher5 * Contact : contact@spboucher.ai6 * Fichier : frontend/src/pages/Employeurs.tsx7 * Rôle : Page « Publier une offre » — dépôt direct par les employeurs8 * (POST /api/employeurs/offres, modération avant publication)9 * Créé : 2026-08-18 Modifié : 2026-08-2210 * =============================================================================11 */12import { FormEvent, useEffect, useState } from "react";1314interface FormState {15 employer: string;16 contact_email: string;17 title: string;18 city: string;19 url: string;20 salary_label: string;21 employment_type: string;22 work_mode: string;23 description: string;24 website: string; // pot de miel anti-robots (doit rester vide)25}2627const EMPTY: FormState = {28 employer: "", contact_email: "", title: "", city: "", url: "",29 salary_label: "", employment_type: "", work_mode: "", description: "",30 website: "",31};3233export default function EmployeursPage() {34 const [form, setForm] = useState<FormState>(EMPTY);35 const [sending, setSending] = useState(false);36 const [done, setDone] = useState<string | null>(null);37 const [err, setErr] = useState<string | null>(null);3839 useEffect(() => {40 document.title = "Publier une offre | Job-Ka — Un service Groupe KA";41 window.scrollTo(0, 0);42 }, []);4344 const set = (k: keyof FormState) =>45 (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>46 setForm((f) => ({ ...f, [k]: e.target.value }));4748 const submit = (e: FormEvent) => {49 e.preventDefault();50 setSending(true);51 setErr(null);52 fetch("/api/employeurs/offres", {53 method: "POST",54 headers: { "Content-Type": "application/json" },55 body: JSON.stringify({56 ...form,57 employment_type: form.employment_type || null,58 work_mode: form.work_mode || null,59 }),60 })61 .then(async (r) => {62 if (!r.ok) {63 const body = await r.json().catch(() => null);64 throw new Error(body?.detail?.[0]?.msg ?? body?.detail ?? `Erreur ${r.status}`);65 }66 return r.json();67 })68 .then((d) => {69 setDone(d.message ?? "Offre reçue — merci !");70 setForm(EMPTY);71 })72 .catch((ex) => setErr(String(ex?.message ?? ex)))73 .finally(() => setSending(false));74 };7576 return (77 <div className="container contact-page">78 <span className="kicker">Employeurs</span>79 <h1>80 Publiez votre offre <span className="hl">directement</span>81 </h1>82 <p className="lede">83 Job-Ka référence les offres à la source : pages carrières des84 employeurs québécois d'abord, portails ensuite. Vous êtes un employeur85 sans page carrière connectée ? Déposez votre offre ici — elle est86 vérifiée par notre équipe avant publication, puis affichée avec le87 badge « Offre directe employeur ». C'est gratuit.88 </p>8990 <ul className="muted" style={{ lineHeight: 1.8, marginBottom: 24 }}>91 <li>Votre offre reste <b>traçable</b> : elle renvoie vers votre site si vous fournissez un lien.</li>92 <li>Si votre entreprise utilise un ATS (Workday, Lever, Njoyn, Taleo…), écrivez-nous :93 nous pouvons <b>connecter votre page carrière</b> et synchroniser toutes vos offres automatiquement.</li>94 <li>Le courriel de contact sert uniquement à la modération — il n'est jamais affiché.</li>95 </ul>9697 {done ? (98 <div className="card" style={{ padding: 24 }}>99 <h3 style={{ marginTop: 0 }}>Offre reçue ✓</h3>100 <p>{done}</p>101 <button className="btn" type="button" onClick={() => setDone(null)}>102 Déposer une autre offre103 </button>104 </div>105 ) : (106 <form onSubmit={submit} className="card depot-form"107 style={{ padding: 24, display: "grid", gap: 14, maxWidth: 760 }}>108 <div className="depot-grid">109 <label>Entreprise *110 <input required minLength={2} maxLength={120}111 value={form.employer} onChange={set("employer")} />112 </label>113 <label>Courriel de contact *114 <input required type="email" maxLength={160}115 value={form.contact_email} onChange={set("contact_email")} />116 </label>117 <label>Titre du poste *118 <input required minLength={3} maxLength={160}119 value={form.title} onChange={set("title")} />120 </label>121 <label>Ville122 <input maxLength={80} value={form.city} onChange={set("city")} />123 </label>124 <label>Lien vers l'offre (votre site)125 <input type="url" placeholder="https://…" maxLength={400}126 value={form.url} onChange={set("url")} />127 </label>128 <label>Salaire (texte libre)129 <input maxLength={120} placeholder="ex. 25 $ à 30 $/h"130 value={form.salary_label} onChange={set("salary_label")} />131 </label>132 <label>Type d'emploi133 <select value={form.employment_type} onChange={set("employment_type")}>134 <option value="">—</option>135 <option value="temps_plein">Temps plein</option>136 <option value="temps_partiel">Temps partiel</option>137 <option value="contractuel">Contractuel</option>138 <option value="stage">Stage</option>139 <option value="saisonnier">Saisonnier</option>140 </select>141 </label>142 <label>Mode de travail143 <select value={form.work_mode} onChange={set("work_mode")}>144 <option value="">—</option>145 <option value="presentiel">Présentiel</option>146 <option value="hybride">Hybride</option>147 <option value="teletravail">Télétravail</option>148 </select>149 </label>150 </div>151 <label>Description du poste * <span className="muted">(30 caractères minimum)</span>152 <textarea required minLength={30} maxLength={20000} rows={10}153 value={form.description} onChange={set("description")} />154 </label>155 {/* pot de miel : caché aux humains, rempli par les robots */}156 <input type="text" name="website" value={form.website}157 onChange={set("website")} autoComplete="off" tabIndex={-1}158 style={{ position: "absolute", left: "-9999px" }} aria-hidden="true" />159 {err && <p style={{ color: "var(--danger, #b3423a)" }}>{err}</p>}160 <div>161 <button className="btn btn-primary" type="submit" disabled={sending}>162 {sending ? "Envoi…" : "Soumettre l'offre pour modération"}163 </button>164 </div>165 <p className="muted" style={{ fontSize: 13, margin: 0 }}>166 En soumettant, vous confirmez être autorisé à publier cette offre.167 Les offres sont vérifiées manuellement avant d'apparaître sur Job-Ka.168 </p>169 </form>170 )}171 </div>172 );173}174