SPB Git forge

spb/immo-ka

Public

Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)

112commits 1branches 0releases
125.4 MBsize
maindefault branch
13 days agolast push
Python 47.5% HTML 27.9% TypeScript 15.5% CSS 7.2% JavaScript 2%
9.1 KB · 242 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// pages/Profil.tsx : profil utilisateur — carte de membre Groupe KA (KA-ID),5//   profil Groupe KA en lecture seule (saisi au hub groupe-ka.com), actions6// -----------------------------------------------------------------------------7import { useEffect, useState } from "react";8import { useNavigate } from "react-router-dom";9import { fetchMe, logout } from "../api";10import type { Socials, User } from "../api";11import { Ico } from "../components/Icons";1213const fmtEpoch = (ts: number | null | undefined): string => {14  if (!ts) return "—";15  return new Date(ts * 1000).toLocaleDateString("fr-CA", {16    day: "numeric", month: "long", year: "numeric",17  }).replace(/^1 /, "1ᵉʳ ");18};1920const SOCIAL_BASE: Record<string, string> = {21  instagram: "https://www.instagram.com/",22  facebook: "https://www.facebook.com/",23  x: "https://x.com/",24  linkedin: "https://www.linkedin.com/in/",25  tiktok: "https://www.tiktok.com/@",26  youtube: "https://www.youtube.com/@",27};2829/** « @pseudo » ou « pseudo » -> URL complète de la plateforme ; URL laissée telle quelle */30function socialUrl(key: string, value: string): string {31  const v = value.trim();32  if (/^https?:\/\//i.test(v)) return v;33  if (key === "website") return `https://${v}`;34  return (SOCIAL_BASE[key] ?? "https://") + v.replace(/^@/, "");35}3637const SOCIAL_FIELDS: { key: keyof Socials; label: string }[] = [38  { key: "instagram", label: "Instagram" },39  { key: "facebook", label: "Facebook" },40  { key: "x", label: "X (Twitter)" },41  { key: "linkedin", label: "LinkedIn" },42  { key: "tiktok", label: "TikTok" },43  { key: "youtube", label: "YouTube" },44];4546/** Profil géré au HUB Groupe KA — LECTURE SEULE (l'édition se fait sur47 *  groupe-ka.com/compte, une seule saisie pour toutes les plateformes). */48function HubProfileSection({ me }: { me: User }) {49  const socialLinks = SOCIAL_FIELDS.filter((s) => (me.socials?.[s.key] ?? "").trim());50  return (51    <section className="hub-profile">52      <h3>Mon profil Groupe KA</h3>53      {me.bio && <p className="hub-bio">{me.bio}</p>}54      <div className="pub-meta">55        {(me.job_title || me.company) && (56          <span className="stat-chip">57            {[me.job_title, me.company].filter(Boolean).join(" · ")}58          </span>59        )}60        {me.city && <span className="stat-chip">{me.city}</span>}61        {me.age != null && <span className="stat-chip">{me.age} ans</span>}62        {me.website && (63          <a className="stat-chip" href={socialUrl("website", me.website)}64             target="_blank" rel="noopener noreferrer">Site web ↗</a>65        )}66        {socialLinks.map((s) => (67          <a key={s.key} className="stat-chip hub-social-chip"68             href={socialUrl(s.key, me.socials![s.key]!)}69             target="_blank" rel="noopener noreferrer" title={s.label}>70            <Ico name={s.key} size={15} /> {s.label}71          </a>72        ))}73      </div>74      <div>75        <a className="btn btn-primary"76           href="https://www.groupe-ka.com/compte"77           target="_blank" rel="noopener noreferrer">78          Modifier mon profil sur groupe-ka.com ↗79        </a>80      </div>81      <p className="hub-hint">82        Votre profil est géré au niveau du groupe : une seule saisie, visible83        sur toutes les plateformes du Groupe KA.84      </p>85    </section>86  );87}8889export default function ProfilPage() {90  const [me, setMe] = useState<User | null | undefined>(undefined); // undefined = chargement91  const [copied, setCopied] = useState(false);92  const nav = useNavigate();9394  useEffect(() => {95    fetchMe().then((r) => setMe(r.user)).catch(() => setMe(null));96  }, []);9798  const copyKaId = async () => {99    if (!me?.ka_id) return;100    try {101      await navigator.clipboard.writeText(me.ka_id);102      setCopied(true);103      setTimeout(() => setCopied(false), 1800);104    } catch { /* presse-papiers indisponible : tant pis */ }105  };106107  if (me === undefined) {108    return <div className="container profil"><div className="notice">Chargement…</div></div>;109  }110111  if (me === null) {112    return (113      <div className="container profil">114        <span className="kicker">Mon compte</span>115        <h1>Connectez-vous pour <span className="hl">votre profil</span>.</h1>116        <p className="lede">117          Connectez-vous avec votre compte <b>KA ID</b> — vous recevrez votre118          identifiant de membre, valide dans tout l'écosystème Groupe KA.119        </p>120        <a className="btn btn-primary" href="/api/auth/ka/login?next=%2Fprofil">121          Se connecter avec KA ID122        </a>123      </div>124    );125  }126127  return (128    <div className="container profil">129      <span className="kicker">Mon compte</span>130      <h1>131        {me.name ? <>Salut, <span className="hl">{me.name.split(" ")[0]}</span>.</>132                 : <>Votre <span className="hl">profil</span>.</>}133      </h1>134135      {/* ——— Carte de membre Groupe KA ——— */}136      <div className="pc" role="img" aria-label={`Carte de membre ${me.ka_id || me.sub}`}>137        <div className="pc-watermark" aria-hidden="true">KA</div>138        <div className="pc-head">139          <span className="pc-brand">Groupe <span className="pc-ka">KA</span></span>140          <span className="pc-label">Carte de membre · Groupe KA</span>141        </div>142        <div className="pc-id-block">143          <span className="pc-id-label">KA-ID</span>144          <span className="pc-id">{me.ka_id || "—"}</span>145        </div>146        <div className="pc-foot">147          <div className="pc-holder">148            <span className="pc-holder-name">{me.name || me.email}</span>149            {me.role_label && (150              <span className="pc-role-badge">{me.role_label}</span>151            )}152            <span className="pc-holder-since">Membre depuis le {fmtEpoch(me.created_at)}</span>153          </div>154          {me.picture && (155            <img className="pc-avatar" src={me.picture} alt="" referrerPolicy="no-referrer" />156          )}157        </div>158        <div className="pc-strip" aria-hidden="true">159          {Array.from({ length: 28 }).map((_, i) => <i key={i} />)}160        </div>161      </div>162163      <button className={`btn btn-ghost pc-copy ${copied ? "ok" : ""}`} onClick={copyKaId}>164        {copied ? "✓ Copié" : "Copier mon KA-ID"}165      </button>166167      {/* ——— Informations ——— */}168      <section className="profil-grid">169        <div className="pg-item">170          <span className="pg-label">Nom</span>171          <span className="pg-value">{me.name || "—"}</span>172        </div>173        <div className="pg-item">174          <span className="pg-label">Courriel</span>175          <span className="pg-value">{me.email || "—"}</span>176        </div>177        <div className="pg-item">178          <span className="pg-label">Identifiant membre</span>179          <span className="pg-value mono">{me.ka_id || me.sub}</span>180        </div>181        <div className="pg-item">182          <span className="pg-label">Connexion</span>183          <span className="pg-value">184            {me.provider === "ka-id" ? "KA ID (groupe-ka.com)" : "Compte Google"}185          </span>186        </div>187        {me.city && (188          <div className="pg-item">189            <span className="pg-label">Ville</span>190            <span className="pg-value">{me.city}</span>191          </div>192        )}193        {me.role_label && (194          <div className="pg-item">195            <span className="pg-label">Statut</span>196            <span className="pg-value">{me.role_label}</span>197          </div>198        )}199        <div className="pg-item">200          <span className="pg-label">Membre depuis</span>201          <span className="pg-value">{fmtEpoch(me.created_at)}</span>202        </div>203        <div className="pg-item">204          <span className="pg-label">Dernière connexion</span>205          <span className="pg-value">{fmtEpoch(me.last_login)}</span>206        </div>207      </section>208209      {me.profile_source === "groupe-ka" && <HubProfileSection me={me} />}210211      {me.profile_source === "groupe-ka" && me.public && me.public_url && (212        <p className="profil-note">213          Votre profil public :{" "}214          <a href={me.public_url} target="_blank" rel="noopener noreferrer" className="mono">215            {me.public_url.replace(/^https?:\/\//, "")}216          </a>{" "}217          — la visibilité se gère sur groupe-ka.com/compte.218        </p>219      )}220221      <p className="profil-note">222        Votre <b>KA-ID</b> est votre identifiant unique dans l'écosystème{" "}223        <a href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer">224          Groupe KA225        </a>{" "}226        — il vous suit sur toutes les plateformes du groupe. Immo-Ka ne conserve227        que les informations de ce profil ; rien d'autre, et jamais revendues.228      </p>229230      {/* ——— Actions ——— */}231      <div className="profil-actions">232        <button233          className="btn btn-ghost"234          onClick={async () => { await logout(); nav("/"); window.location.reload(); }}235        >236          Se déconnecter237        </button>238      </div>239    </div>240  );241}242