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%
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