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%
4.1 KB · 79 lines tsx
Raw Blame History
1/**2 * =============================================================================3 * Job·Ka — Groupe KA4 * Auteur  : Simon-Pierre Boucher5 * Contact : contact@spboucher.ai6 * Fichier : frontend/src/fiche/JobHero.tsx7 * Rôle    : Héro de la fiche — logo + employeur · lieu, salaire en exergue8 *           (+ équivalent annuel), capsules (marché, nouveau, offre directe),9 *           titre du poste (H1) + résumé, actions (favoris · partager ·10 *           Postuler). Ordre DOM = ordre visuel.11 * Créé    : 2026-09-0712 * =============================================================================13 */14import { useState } from "react";15import { Job, daysSince, displayTitle, formatSalary, yearlyEquiv } from "../api";16import { Ico, IcoHeart } from "../components/Icons";17import { ComparaisonSalaire, ligneResume } from "./synthese";18import { NBSP } from "./ui";1920export function SalaryCapsule({ cmp, short = false }: { cmp: ComparaisonSalaire | null; short?: boolean }) {21  if (!cmp) return null;22  return (23    <span className={`jk-capsule ${cmp.tone}`}>24      {short ? <b>{cmp.court}</b> : <><b>{cmp.label}</b><span aria-hidden="true">·</span>{cmp.court}</>}25    </span>26  );27}2829export function EmployerLogo({ job, size = 56 }: { job: Job; size?: number }) {30  const [ok, setOk] = useState(true);31  if (!job.company_logo || !ok)32    return <span className="jk-logo jk-logo-fallback" style={{ width: size, height: size }} aria-hidden="true">{(job.employer || "?").charAt(0).toUpperCase()}</span>;33  return <img className="jk-logo" src={job.company_logo} alt={`Logo ${job.employer}`} style={{ width: size, height: size }} onError={() => setOk(false)} />;34}3536export default function JobHero({ job, cmp, fav, onFav, onShare, actionsRef }: {37  job: Job; cmp: ComparaisonSalaire | null; fav: boolean;38  onFav: () => void; onShare: () => void; actionsRef: React.RefObject<HTMLDivElement>;39}) {40  const salaire = formatSalary(job);41  const yearly = yearlyEquiv(job);42  const days = daysSince(job.date_posted);43  const resume = ligneResume(job);44  return (45    <header className="jk-hero" aria-label="Résumé de l'offre">46      <div className="jk-hero-emp">47        <EmployerLogo job={job} />48        <div style={{ minWidth: 0 }}>49          <div className="jk-kicker">{job.employer}{job.location_label ? ` — ${job.location_label}` : job.city ? ` — ${job.city}` : ""}</div>50          <div className="jk-price">51            {salaire ?? <span style={{ fontSize: "0.62em", fontWeight: 600, color: "var(--jk-text-2)" }}>Salaire non affiché</span>}52            {yearly && <small>{yearly}</small>}53          </div>54        </div>55      </div>56      <div className="jk-price-row" style={{ marginTop: 8 }}>57        <SalaryCapsule cmp={cmp} />58        {days != null && days <= 3 && <span className="jk-capsule brand">{days === 0 ? "Aujourd'hui" : "Nouveau"}</span>}59        {job.is_direct !== false && <span className="jk-capsule ok">Offre directe</span>}60        {job.active === 0 && <span className="jk-capsule high">Offre retirée</span>}61        {job.ka_reco && <span className="jk-capsule brand">Recommandé pour vous</span>}62      </div>63      <h1 className="jk-h1 jk-h1-big">64        {displayTitle(job)}65        {resume.length > 0 && <span className="jk-city">{resume.join(" · ")}</span>}66      </h1>67      <div className="jk-actions" ref={actionsRef}>68        <button type="button" className={`jk-btn jk-btn-ghost jk-btn-icon ${fav ? "on" : ""}`} aria-pressed={fav}69                aria-label={fav ? "Retirer des favoris" : "Ajouter aux favoris"} onClick={onFav}><IcoHeart size={19} filled={fav} /></button>70        <button type="button" className="jk-btn jk-btn-ghost jk-btn-icon" aria-label="Partager" onClick={onShare}><Ico name="share" size={18} /></button>71        <a className="jk-btn jk-btn-primary" href={job.apply_url || job.url} target="_blank" rel="noopener noreferrer">72          Postuler <Ico name="external" size={16} /><span className="visually-hidden"> chez {job.employer}</span>73        </a>74      </div>75      <p className="jk-fine" style={{ margin: "2px 0 0" }}>La candidature se fait chez l'employeur{job.ats ? ` (${job.ats})` : ""} — Job·Ka n'est pas un intermédiaire.{NBSP}</p>76    </header>77  );78}79