SPB Git forge

spb/job-ka

Public
226commits 1branches 0releases
37.5 MBsize
maindefault branch
10 h agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%
4.9 KB · 140 lines tsx
Raw Blame History
1/**2 * =============================================================================3 * Job·Ka — Groupe KA4 * Auteur  : Simon-Pierre Boucher5 * Contact : contact@spboucher.ai6 * Fichier : frontend/src/favorites.tsx7 * Rôle    : Favoris ♥ « Mon univers Ka » — le hub Groupe KA est le magasin8 *           central des favoris du groupe (aucun stockage navigateur) :9 *           lecture/écriture via l'API locale /api/favorites qui relaie au10 *           hub. FavProvider charge la liste une fois ; FavButton = cœur sur11 *           les cartes d'offres et la fiche. Non connecté → redirection vers12 *           la connexion KA ID avec retour à la page courante.13 * Créé    : 2026-08-23   Modifié : 2026-08-2314 * =============================================================================15 */16import {17  ReactNode, createContext, useCallback, useContext,18  useEffect, useMemo, useState,19} from "react";20import { displayTitle, formatSalary, Job } from "./api";2122/** Item de favori tel que poussé au hub Groupe KA. */23export interface FavItem {24  item_id: string;25  title: string;26  subtitle?: string;27  price_label?: string;28  image_url?: string;29  url?: string;30}3132interface FavState {33  ready: boolean;              // premier chargement terminé34  connected: boolean;          // session KA ID active35  ids: Set<string>;            // uid des offres en favoris36  items: FavItem[];37  toggle: (item: FavItem) => void;38}3940const FavContext = createContext<FavState>({41  ready: false, connected: false, ids: new Set(), items: [], toggle: () => {},42});4344/** Offre -> item de favori du hub (titre du poste, employeur · ville, salaire). */45export function jobFavItem(job: Job): FavItem {46  return {47    item_id: job.uid,48    title: displayTitle(job) || "Offre d'emploi",49    subtitle: [job.employer, job.city || job.region]50      .filter(Boolean).join(" · "),51    price_label: formatSalary(job) || "",52    image_url: job.company_logo || "",53    url: `https://www.job-ka.com/emploi/${encodeURIComponent(job.uid)}`,54  };55}5657/** Redirection vers la connexion KA ID, retour à la page courante. */58export function goLogin() {59  const next = encodeURIComponent(60    window.location.pathname + window.location.search);61  window.location.assign(`/api/auth/ka/login?next=${next}`);62}6364export function FavProvider({ children }: { children: ReactNode }) {65  const [ready, setReady] = useState(false);66  const [connected, setConnected] = useState(false);67  const [items, setItems] = useState<FavItem[]>([]);6869  useEffect(() => {70    fetch("/api/favorites", { credentials: "same-origin" })71      .then(async (res) => {72        if (res.status === 401) return;          // pas connecté : cœurs vides73        if (!res.ok) throw new Error(`API ${res.status}`);74        const data = (await res.json()) as { items?: FavItem[] };75        setConnected(true);76        setItems(data.items ?? []);77      })78      .catch(() => {})                            // hub muet : cœurs vides79      .finally(() => setReady(true));80  }, []);8182  const toggle = useCallback((item: FavItem) => {83    if (!connected) {84      goLogin();                // non connecté : connexion KA ID puis retour85      return;86    }87    const prev = items;88    const on = !prev.some((i) => i.item_id === item.item_id);89    // optimiste : le hub confirme (sinon on rétablit)90    setItems(on ? [...prev, item]91                : prev.filter((i) => i.item_id !== item.item_id));92    fetch("/api/favorites/toggle", {93      method: "POST",94      credentials: "same-origin",95      headers: { "Content-Type": "application/json" },96      body: JSON.stringify({ on, item }),97    })98      .then((res) => { if (!res.ok) throw new Error(`API ${res.status}`); })99      .catch(() => setItems(prev));100  }, [connected, items]);101102  const ids = useMemo(103    () => new Set(items.map((i) => i.item_id)), [items]);104105  return (106    <FavContext.Provider value={{ ready, connected, ids, items, toggle }}>107      {children}108    </FavContext.Provider>109  );110}111112export const useFav = () => useContext(FavContext);113114/** Cœur ♥ — cartes d'offres (par défaut) et fiche d'offre (`big`). */115export function FavButton({ job, big = false }: { job: Job; big?: boolean }) {116  const { ids, toggle } = useFav();117  const on = ids.has(job.uid);118  const label = on119    ? "Retirer de mes favoris (Mon univers Ka)"120    : "Ajouter à mes favoris (Mon univers Ka)";121  return (122    <button123      type="button"124      className={`fav-btn${on ? " on" : ""}${big ? " big" : ""}`}125      aria-label={label}126      aria-pressed={on}127      title={label}128      onClick={(e) => {129        e.preventDefault();       // la carte entière est un lien130        e.stopPropagation();131        toggle(jobFavItem(job));132      }}133    >134      <svg viewBox="0 0 24 24" aria-hidden="true">135        <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />136      </svg>137    </button>138  );139}140