SPB Git

spb/toit-ka Public

Toit-Ka — louer ou acheter un toit au Québec, un seul endroit (fusion Lou-Ka × Immo-Ka) — www.toit-ka.com

Python 40.2% TypeScript 39% CSS 20.2% HTML 0.7%
8.8 KB · 238 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Author: Simon-Pierre Boucher3// Contact: contact@spboucher.ai4// Project: Toit-Ka5// pages/Profil.tsx : profil utilisateur — carte de membre Groupe KA (KA-ID),6//   profil Groupe KA en lecture seule (saisi au hub groupe-ka.com), actions.7// -----------------------------------------------------------------------------8import { useEffect, useState } from "react";9import { useNavigate } from "react-router-dom";10import { fetchMe, logout } from "../api";11import type { Socials, User } from "../api";12import { Ico } from "../components/Icons";1314const fmtEpoch = (ts: number | null | undefined): string => {15  if (!ts) return "—";16  return new Date(ts * 1000).toLocaleDateString("fr-CA", {17    day: "numeric", month: "long", year: "numeric",18  }).replace(/^1 /, "1ᵉʳ ");19};2021const SOCIAL_BASE: Record<string, string> = {22  instagram: "https://www.instagram.com/",23  facebook: "https://www.facebook.com/",24  x: "https://x.com/",25  linkedin: "https://www.linkedin.com/in/",26  tiktok: "https://www.tiktok.com/@",27  youtube: "https://www.youtube.com/@",28};2930function 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 (édition sur groupe-ka.com). */47function HubProfileSection({ me }: { me: User }) {48  const socialLinks = SOCIAL_FIELDS.filter((s) => (me.socials?.[s.key] ?? "").trim());49  return (50    <section className="hub-profile">51      <h3>Mon profil Groupe KA</h3>52      {me.bio && <p className="hub-bio">{me.bio}</p>}53      <div className="pub-meta">54        {(me.job_title || me.company) && (55          <span className="stat-chip">56            {[me.job_title, me.company].filter(Boolean).join(" · ")}57          </span>58        )}59        {me.city && <span className="stat-chip">{me.city}</span>}60        {me.age != null && <span className="stat-chip">{me.age} ans</span>}61        {me.website && (62          <a className="stat-chip" href={socialUrl("website", me.website)}63             target="_blank" rel="noopener noreferrer">Site web ↗</a>64        )}65        {socialLinks.map((s) => (66          <a key={s.key} className="stat-chip hub-social-chip"67             href={socialUrl(s.key, me.socials![s.key]!)}68             target="_blank" rel="noopener noreferrer" title={s.label}>69            <Ico name={s.key} size={15} /> {s.label}70          </a>71        ))}72      </div>73      <div>74        <a className="btn btn-primary"75           href="https://www.groupe-ka.com/compte"76           target="_blank" rel="noopener noreferrer">77          Modifier mon profil sur groupe-ka.com ↗78        </a>79      </div>80      <p className="hub-hint">81        Votre profil est géré au niveau du groupe : une seule saisie, visible82        sur toutes les plateformes du Groupe KA.83      </p>84    </section>85  );86}8788export default function ProfilPage() {89  const [me, setMe] = useState<User | null | undefined>(undefined);90  const [copied, setCopied] = useState(false);91  const nav = useNavigate();9293  useEffect(() => {94    fetchMe().then((r) => setMe(r.user)).catch(() => setMe(null));95  }, []);9697  const copyKaId = async () => {98    if (!me?.ka_id) return;99    try {100      await navigator.clipboard.writeText(me.ka_id);101      setCopied(true);102      setTimeout(() => setCopied(false), 1800);103    } catch { /* presse-papiers indisponible */ }104  };105106  if (me === undefined) {107    return <div className="container profil"><div className="notice">Chargement…</div></div>;108  }109110  if (me === null) {111    return (112      <div className="container profil">113        <span className="kicker">Mon compte</span>114        <h1>Connectez-vous pour <span className="hl">votre profil</span>.</h1>115        <p className="lede">116          Connectez-vous avec votre compte <b>KA ID</b> — vous recevrez votre117          identifiant de membre, valide dans tout l'écosystème Groupe KA.118        </p>119        <a className="btn btn-primary" href="/api/auth/ka/login?next=%2Fprofil">120          Se connecter avec KA ID121        </a>122      </div>123    );124  }125126  return (127    <div className="container profil">128      <span className="kicker">Mon compte</span>129      <h1>130        {me.name ? <>Salut, <span className="hl">{me.name.split(" ")[0]}</span>.</>131                 : <>Votre <span className="hl">profil</span>.</>}132      </h1>133134      {/* ——— Carte de membre Groupe KA ——— */}135      <div className="pc" role="img" aria-label={`Carte de membre ${me.ka_id || me.sub}`}>136        <div className="pc-watermark" aria-hidden="true">KA</div>137        <div className="pc-head">138          <span className="pc-brand">Groupe <span className="pc-ka">KA</span></span>139          <span className="pc-label">Carte de membre · Groupe KA</span>140        </div>141        <div className="pc-id-block">142          <span className="pc-id-label">KA-ID</span>143          <span className="pc-id">{me.ka_id || "—"}</span>144        </div>145        <div className="pc-foot">146          <div className="pc-holder">147            <span className="pc-holder-name">{me.name || me.email}</span>148            {me.role_label && (149              <span className="pc-role-badge">{me.role_label}</span>150            )}151            <span className="pc-holder-since">Membre depuis le {fmtEpoch(me.created_at)}</span>152          </div>153          {me.picture && (154            <img className="pc-avatar" src={me.picture} alt="" referrerPolicy="no-referrer" />155          )}156        </div>157        <div className="pc-strip" aria-hidden="true">158          {Array.from({ length: 28 }).map((_, i) => <i key={i} />)}159        </div>160      </div>161162      <button className={`btn btn-ghost pc-copy ${copied ? "ok" : ""}`} onClick={copyKaId}>163        {copied ? "✓ Copié" : "Copier mon KA-ID"}164      </button>165166      {/* ——— Informations ——— */}167      <section className="profil-grid">168        <div className="pg-item">169          <span className="pg-label">Nom</span>170          <span className="pg-value">{me.name || "—"}</span>171        </div>172        <div className="pg-item">173          <span className="pg-label">Courriel</span>174          <span className="pg-value">{me.email || "—"}</span>175        </div>176        <div className="pg-item">177          <span className="pg-label">Identifiant membre</span>178          <span className="pg-value mono">{me.ka_id || me.sub}</span>179        </div>180        <div className="pg-item">181          <span className="pg-label">Connexion</span>182          <span className="pg-value">KA ID (groupe-ka.com)</span>183        </div>184        {me.city && (185          <div className="pg-item">186            <span className="pg-label">Ville</span>187            <span className="pg-value">{me.city}</span>188          </div>189        )}190        {me.role_label && (191          <div className="pg-item">192            <span className="pg-label">Statut</span>193            <span className="pg-value">{me.role_label}</span>194          </div>195        )}196        <div className="pg-item">197          <span className="pg-label">Membre depuis</span>198          <span className="pg-value">{fmtEpoch(me.created_at)}</span>199        </div>200        <div className="pg-item">201          <span className="pg-label">Dernière connexion</span>202          <span className="pg-value">{fmtEpoch(me.last_login)}</span>203        </div>204      </section>205206      {me.profile_source === "groupe-ka" && <HubProfileSection me={me} />}207208      {me.profile_source === "groupe-ka" && me.public && me.public_url && (209        <p className="profil-note">210          Votre profil public :{" "}211          <a href={me.public_url} target="_blank" rel="noopener noreferrer" className="mono">212            {me.public_url.replace(/^https?:\/\//, "")}213          </a>{" "}214          — la visibilité se gère sur groupe-ka.com/compte.215        </p>216      )}217218      <p className="profil-note">219        Votre <b>KA-ID</b> est votre identifiant unique dans l'écosystème{" "}220        <a href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer">221          Groupe KA222        </a>{" "}223        — il vous suit sur toutes les plateformes du groupe. Toit-Ka ne conserve224        que les informations de ce profil ; rien d'autre, et jamais revendues.225      </p>226227      <div className="profil-actions">228        <button229          className="btn btn-ghost"230          onClick={async () => { await logout(); nav("/"); window.location.reload(); }}231        >232          Se déconnecter233        </button>234      </div>235    </div>236  );237}238