SPB Git forge

spb/job-ka

Public
229commits 1branches 0releases
38.1 MBsize
maindefault branch
2 min agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%
7.1 KB · 174 lines tsx
Raw Blame History
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