Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.
TypeScript 90.2%
JavaScript 3.5%
Python 3.4%
CSS 1.9%
HTML 0.6%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2/**3 * Bouton ♥ « Mon univers Ka » — favoris unifiés du Groupe KA (KA ID).4 * · Connecté : bascule le favori au hub via POST /api/favorites/toggle5 * (synchrone — le hub groupe-ka.com est le magasin central, rien en local).6 * · Déconnecté : le clic envoie au SSO KA ID (retour sur la page courante).7 * L'état initial est lu via GET /api/favorites, partagé entre tous les boutons8 * de la page (une seule requête, mini-cache module 15 s, invalidé au toggle).9 * Utilisable dans un <Link> parent (stopPropagation + preventDefault).10 */11"use client";12import { useEffect, useState } from "react";13import { usePathname } from "next/navigation";14import { useLang } from "./LangContext";1516export type FavPayload = {17 item_id: string;18 title: string;19 subtitle?: string;20 price_label?: string;21 image_url?: string;22 url?: string;23};2425type FavState = { authed: boolean; ids: string[] };2627// -- mini-cache module : une requête partagée par page, TTL 15 s --------------28const TTL_MS = 15_000;29let shared: { at: number; p: Promise<FavState> } | null = null;3031function loadFavs(): Promise<FavState> {32 const now = Date.now();33 if (shared && now - shared.at < TTL_MS) return shared.p;34 const p: Promise<FavState> = fetch("/api/favorites")35 .then(async (r) => {36 if (r.status === 401) return { authed: false, ids: [] };37 if (!r.ok) throw new Error(String(r.status));38 const d = (await r.json()) as { favorites?: { item_id?: string }[] };39 return {40 authed: true,41 ids: (d.favorites ?? []).map((f) => String(f.item_id ?? "")),42 };43 })44 .catch(() => {45 shared = null; // erreur transitoire : jamais mise en cache46 return { authed: true, ids: [] };47 });48 shared = { at: now, p };49 return p;50}5152export default function FavButton({53 item,54 size = "sm",55 className = "",56}: {57 item: FavPayload;58 size?: "sm" | "lg";59 className?: string;60}) {61 const { lang } = useLang();62 const fr = lang === "fr";63 const path = usePathname();64 const [authed, setAuthed] = useState<boolean | null>(null);65 const [fav, setFav] = useState(false);66 const [busy, setBusy] = useState(false);67 const [flash, setFlash] = useState(false); // échec hub — bref signal visuel6869 useEffect(() => {70 let alive = true;71 loadFavs().then((st) => {72 if (!alive) return;73 setAuthed(st.authed);74 setFav(st.ids.includes(item.item_id));75 });76 return () => {77 alive = false;78 };79 }, [item.item_id]);8081 const onClick = async (e: React.MouseEvent) => {82 // le bouton vit parfois dans un <Link> (carte de résultat) : ne pas naviguer83 e.preventDefault();84 e.stopPropagation();85 if (busy) return;86 if (authed === false) {87 window.location.href = `/api/auth/ka/login?next=${encodeURIComponent(path || "/")}`;88 return;89 }90 setBusy(true);91 try {92 const r = await fetch("/api/favorites/toggle", {93 method: "POST",94 headers: { "Content-Type": "application/json" },95 body: JSON.stringify({ ...item, action: fav ? "remove" : "add" }),96 });97 if (r.status === 401) {98 window.location.href = `/api/auth/ka/login?next=${encodeURIComponent(path || "/")}`;99 return;100 }101 if (!r.ok) throw new Error(String(r.status));102 const d = (await r.json()) as { favorited?: boolean };103 setFav(!!d.favorited);104 shared = null; // les autres boutons reliront une liste fraîche105 } catch {106 setFlash(true);107 setTimeout(() => setFlash(false), 1200);108 } finally {109 setBusy(false);110 }111 };112113 const label = fav114 ? fr115 ? "Retirer de mes favoris"116 : "Remove from my favourites"117 : fr118 ? "Ajouter à mes favoris — Mon univers Ka"119 : "Add to my favourites — My Ka universe";120 const dims =121 size === "lg" ? "h-11 w-11 text-[19px]" : "h-8 w-8 text-[14px]";122123 return (124 <button125 type="button"126 onClick={onClick}127 disabled={busy}128 aria-pressed={fav}129 aria-label={label}130 title={131 flash132 ? fr133 ? "Hub Groupe KA injoignable — réessayez"134 : "Groupe KA hub unreachable — try again"135 : label136 }137 className={`flex shrink-0 items-center justify-center rounded-full border-[1.5px] border-ink leading-none transition-all ${dims} ${138 fav139 ? "bg-ink text-lime hover:bg-green-deep"140 : "bg-surface text-ink-3 hover:bg-lime-soft hover:text-ink"141 } ${busy ? "opacity-60" : ""} ${flash ? "!border-red-600 !text-red-600" : ""} ${className}`}142 >143 <span aria-hidden="true" className="translate-y-[-1px]">144 {fav ? "♥" : "♡"}145 </span>146 </button>147 );148}149