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