SPB Git forge

spb/job-ka

Public
229commits 1branches 0releases
38.1 MBsize
maindefault branch
1 h agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%
5.5 KB · 91 lines tsx
Raw Blame History
1/**2 * =============================================================================3 * Job·Ka — Groupe KA4 * Auteur  : Simon-Pierre Boucher5 * Contact : contact@spboucher.ai6 * Fichier : frontend/src/fiche/JobFacts.tsx7 * Rôle    : « Le poste » — grille icône/valeur (mode, type, séniorité, langue,8 *           expérience, scolarité, catégorie, région), rangées (publiée, date9 *           limite, plateforme, lieu), avantages, description (tronquée),10 *           champs bruts des exigences en accordéon.11 * Créé    : 2026-09-0712 * =============================================================================13 */14import { ReactNode, useState } from "react";15import { Job, LANG_FR, MODE_FR, SENIORITY_FR, TYPE_FR, agoLabel, daysSince, formatDate } from "../api";16import { Ico } from "../components/Icons";17import { Accordion, MoreButton, SectionCard } from "./ui";1819export default function JobFacts({ job }: { job: Job }) {20  const [open, setOpen] = useState(false);21  const r = job.requirements ?? {};22  const facts: { ico: ReactNode; v: string; l: string }[] = [];23  if (job.work_mode) facts.push({ ico: <Ico name="home" size={17} />, v: MODE_FR[job.work_mode] ?? job.work_mode, l: "Mode de travail" });24  if (job.employment_type) facts.push({ ico: <Ico name="calendar" size={17} />, v: TYPE_FR[job.employment_type] ?? job.employment_type, l: "Type d'emploi" });25  if (job.seniority) facts.push({ ico: <Ico name="layers" size={17} />, v: SENIORITY_FR[job.seniority] ?? job.seniority, l: "Niveau" });26  if (job.language) facts.push({ ico: <Ico name="book" size={17} />, v: LANG_FR[job.language] ?? job.language, l: "Langue de l'offre" });27  if (typeof r.experience_years === "number") facts.push({ ico: <Ico name="history" size={17} />, v: `${r.experience_years} an${r.experience_years > 1 ? "s" : ""} min.`, l: "Expérience exigée" });28  if (typeof r.education === "string" && r.education) facts.push({ ico: <Ico name="school" size={17} />, v: r.education, l: "Scolarité exigée" });29  if (Array.isArray(r.languages) && r.languages.length) facts.push({ ico: <Ico name="people" size={17} />, v: r.languages.length === 2 ? "Bilingue" : r.languages.join(", "), l: "Langues exigées" });30  if (job.category) facts.push({ ico: <Ico name="tag" size={17} />, v: job.category, l: "Catégorie" });31  if (job.region) facts.push({ ico: <Ico name="pin" size={17} />, v: job.region, l: "Région" });3233  const days = daysSince(job.date_posted);34  const rows: [string, string][] = [];35  if (job.location_label || job.address) rows.push(["Lieu", job.location_label || [job.address, job.city].filter(Boolean).join(", ")]);36  if (job.date_posted) rows.push(["Publiée", `${formatDate(job.date_posted)}${days != null ? ` (${agoLabel(days)})` : ""}`]);37  if (job.date_deadline) rows.push(["Date limite", formatDate(job.date_deadline)]);38  if (job.ats) rows.push(["Plateforme de recrutement", job.ats]);39  const det = job.details ?? {};40  if (typeof det.industry === "string" && det.industry) rows.push(["Secteur", det.industry as string]);41  if (typeof det.employment_label === "string" && det.employment_label) rows.push(["Statut annoncé", det.employment_label as string]);4243  const texte = (job.description || "").trim();44  const long = texte.length > 700;45  const bruts = Object.entries(r).filter(([k, v]) => !["experience_years", "education", "languages"].includes(k) && v != null && String(v).trim() && typeof v !== "object");4647  return (48    <SectionCard id="poste" title="Le poste">49      {facts.length > 0 && (50        <div className="jk-facts" role="list">51          {facts.map((x) => (52            <div className="jk-fact" role="listitem" key={x.l}>53              <span className="jk-fact-ico" aria-hidden="true">{x.ico}</span>54              <span className="jk-fact-txt"><div className="jk-fact-v" title={x.v}>{x.v}</div><div className="jk-fact-l">{x.l}</div></span>55            </div>56          ))}57        </div>58      )}59      {rows.length > 0 && (60        <div className="jk-facts-rows">61          {rows.map(([k, v]) => <div className="jk-kv" key={k}><span className="k">{k}</span><span className="v">{v}</span></div>)}62        </div>63      )}64      {(job.benefits?.length ?? 0) > 0 && (65        <>66          <h3 className="jk-card-sub jk-subtitle" id="avantages">Avantages annoncés <small>· {job.benefits.length}</small></h3>67          <div className="jk-amen" role="list">68            {job.benefits.map((b) => (69              <div className="jk-amen-it" role="listitem" key={b}><span className="jk-amen-ico" aria-hidden="true"><Ico name="check" size={14} /></span><span className="jk-amen-txt">{b}</span></div>70            ))}71          </div>72        </>73      )}74      <h3 className="jk-card-sub jk-subtitle" id="description">Description du poste</h3>75      {texte ? (76        <>77          <div className={`jk-desc ${long && !open ? "clamped" : ""}`}><div className="jk-desc-body"><p>{texte}</p></div></div>78          {long && <MoreButton onClick={() => setOpen(!open)} expanded={open}>{open ? "Réduire" : "Lire la suite"}</MoreButton>}79        </>80      ) : <p className="jk-fine" style={{ marginTop: 0 }}>L'employeur ne fournit pas de description exploitable — consultez l'offre originale.</p>}81      {bruts.length > 0 && (82        <Accordion title="Autres champs publiés par l'employeur" meta={`${bruts.length}`}>83          <div className="jk-facts-rows" style={{ marginTop: 0 }}>84            {bruts.map(([k, v]) => <div className="jk-kv" key={k}><span className="k">{k}</span><span className="v">{String(v)}</span></div>)}85          </div>86        </Accordion>87      )}88    </SectionCard>89  );90}91