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/components/JobCard.tsx7 * Rôle : Rangée d'offre éditoriale — titre fort, salaire en exergue à8 * droite, métadonnées calmes (mode, type, séniorité, exigences),9 * fraîcheur, cœur ♥ favoris. Zéro carte bordée : filets fins +10 * barre d'accent au survol.11 * Créé : 2026-08-17 Modifié : 2026-08-2512 * =============================================================================13 */14import { Link } from "react-router-dom";15import {16 agoLabel, daysSince, displayTitle, formatSalary, Job,17 MODE_FR, SENIORITY_FR, TYPE_FR, yearlyEquiv,18} from "../api";19import { FavButton } from "../favorites";2021const NEW_DAYS = 3; // « Nouveau » : publiée il y a 3 jours ou moins2223export default function JobCard({ job }: { job: Job }) {24 const salary = formatSalary(job);25 const yearly = yearlyEquiv(job);26 const days = daysSince(job.date_posted);27 const isNew = days !== null && days <= NEW_DAYS;28 const exp = job.requirements?.experience_years;29 const langs = job.requirements?.languages;3031 // métadonnées calmes, séparées par des points médians (pas de soupe de pilules)32 const tags: string[] = [];33 if (job.work_mode) tags.push(MODE_FR[job.work_mode] ?? job.work_mode);34 if (job.employment_type) tags.push(TYPE_FR[job.employment_type] ?? job.employment_type);35 if (job.seniority) tags.push(SENIORITY_FR[job.seniority] ?? job.seniority);36 if (typeof exp === "number") tags.push(`${exp} an${exp > 1 ? "s" : ""} d'exp.`);37 if (langs && langs.length === 2) tags.push("Bilingue");38 if (job.category) tags.push(job.category);3940 return (41 <Link to={`/emploi/${job.uid}`} className="job-row">42 <FavButton job={job} />43 <div className="jr-main">44 <h3 className="jr-title">45 {isNew && <span className="jr-new">Nouveau</span>}46 {job.ka_reco && (47 <span48 className="jr-reco"49 title="Correspond à vos préférences KA ID — gérez-les sur groupe-ka.com/mon-ka"50 >51 Recommandé pour vous52 </span>53 )}54 {displayTitle(job)}55 </h3>56 <p className="jr-org">57 <span className="employer">{job.employer}</span>58 {job.city && <span className="jr-city">{job.city}</span>}59 {job.is_direct !== false && (60 <span61 className="jr-direct"62 title="Offre publiée à la source : page carrière de l'employeur, pas un portail"63 >64 source directe65 </span>66 )}67 </p>68 {tags.length > 0 && (69 <p className="jr-tags">70 {tags.map((t, i) => (71 <span key={i}>{t}</span>72 ))}73 </p>74 )}75 </div>76 <div className="jr-side">77 {salary ? (78 <>79 <span className="jr-salary">{salary}</span>80 {yearly && <span className="jr-yearly">{yearly}</span>}81 </>82 ) : (83 <span className="jr-nosalary">Salaire non affiché</span>84 )}85 <span className="jr-when">86 {days !== null ? `Publiée ${agoLabel(days)}` : job.ats}87 </span>88 </div>89 </Link>90 );91}92