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/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