Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)
Python 47.5%
HTML 27.9%
TypeScript 15.5%
CSS 7.2%
JavaScript 2%
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