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%
3.3 KB · 92 lines tsx
Raw Blame History
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