Repository Public
1// -----------------------------------------------------------------------------2// Food-Ka — Agrégateur de produits d'épicerie (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// informations du compte, actions (déconnexion, confidentialité)6// -----------------------------------------------------------------------------7import { useEffect, useState } from "react";8import { Link, useNavigate } from "react-router-dom";9import { Me, Socials, fetchAuth, logout } from "../api";1011const fmtEpoch = (ts: number | null | undefined): string => {12 if (!ts) return "—";13 return new Date(ts * 1000).toLocaleDateString("fr-CA", {14 day: "numeric", month: "long", year: "numeric",15 }).replace(/^1 /, "1ᵉʳ ");16};1718const providerLabel = (p: string): string =>19 p === "google" ? "KA ID — via Google"20 : p === "email" || p === "password" ? "KA ID — courriel et mot de passe"21 : "KA ID (groupe-ka.com)";2223// --- Profil Groupe KA (géré au hub — lecture seule ici) -----------------------24const SOCIAL_BASE: Record<string, string> = {25 instagram: "https://www.instagram.com/",26 facebook: "https://www.facebook.com/",27 x: "https://x.com/",28 linkedin: "https://www.linkedin.com/in/",29 tiktok: "https://www.tiktok.com/@",30 youtube: "https://www.youtube.com/@",31};3233/** « @pseudo » ou « pseudo » -> URL complète de la plateforme ; URL laissée telle quelle */34function socialUrl(key: string, value: string): string {35 const v = value.trim();36 if (/^https?:\/\//i.test(v)) return v;37 if (key === "website") return `https://${v}`;38 return (SOCIAL_BASE[key] ?? "https://") + v.replace(/^@/, "");39}4041const SOCIAL_FIELDS: { key: keyof Socials; label: string }[] = [42 { key: "instagram", label: "Instagram" },43 { key: "facebook", label: "Facebook" },44 { key: "x", label: "X (Twitter)" },45 { key: "linkedin", label: "LinkedIn" },46 { key: "tiktok", label: "TikTok" },47 { key: "youtube", label: "YouTube" },48];4950/** Profil géré au HUB Groupe KA — LECTURE SEULE (l'édition se fait sur51 * groupe-ka.com/compte, une seule saisie pour les huit plateformes). */52function HubProfileSection({ me }: { me: Me }) {53 const socialLinks = SOCIAL_FIELDS.filter((s) => (me.socials?.[s.key] ?? "").trim());54 return (55 <section className="hub-profile">56 <h3>Mon profil Groupe KA</h3>57 {me.bio && <p className="hub-bio">{me.bio}</p>}58 <div className="pub-meta">59 {(me.job_title || me.company) && (60 <span className="stat-chip">61 {[me.job_title, me.company].filter(Boolean).join(" · ")}62 </span>63 )}64 {me.city && <span className="stat-chip">{me.city}</span>}65 {me.age != null && <span className="stat-chip">{me.age} ans</span>}66 {me.website && (67 <a className="stat-chip" href={socialUrl("website", me.website)}68 target="_blank" rel="noopener noreferrer">Site web ↗</a>69 )}70 {socialLinks.map((s) => (71 <a key={s.key} className="stat-chip"72 href={socialUrl(s.key, me.socials![s.key]!)}73 target="_blank" rel="noopener noreferrer" title={s.label}>74 {s.label}75 </a>76 ))}77 </div>78 <div>79 <a className="btn btn-primary"80 href="https://www.groupe-ka.com/compte"81 target="_blank" rel="noopener noreferrer">82 Modifier mon profil sur groupe-ka.com ↗83 </a>84 </div>85 <p className="pe-hint">86 Votre profil est géré au niveau du groupe : une seule saisie, visible87 sur les huit plateformes du groupe.88 </p>89 </section>90 );91}9293export default function ProfilPage() {94 const [me, setMe] = useState<Me | null | undefined>(undefined); // undefined = chargement95 const [copied, setCopied] = useState(false);96 const nav = useNavigate();9798 useEffect(() => {99 fetchAuth().then(({ user }) => setMe(user));100 }, []);101102 const copyKaId = async () => {103 if (!me?.ka_id) return;104 try {105 await navigator.clipboard.writeText(me.ka_id);106 setCopied(true);107 setTimeout(() => setCopied(false), 1800);108 } catch { /* presse-papiers indisponible : tant pis */ }109 };110111 if (me === undefined) {112 return <div className="container profil"><div className="notice">Chargement…</div></div>;113 }114115 if (me === null) {116 return (117 <div className="container profil">118 <span className="kicker">Mon compte</span>119 <h1>Connectez-vous pour <span className="hl">votre profil</span>.</h1>120 <p className="lede">121 Connectez-vous avec votre <b>KA ID</b> — l'identifiant de membre122 unique du Groupe KA, valide sur toutes les plateformes du groupe.123 </p>124 <div className="profil-actions">125 <a className="btn btn-primary" href="/api/auth/ka/login?next=/profil">126 Se connecter avec KA ID127 </a>128 <a129 className="btn btn-ghost"130 href="https://www.groupe-ka.com/connexion"131 target="_blank"132 rel="noopener noreferrer"133 >134 Créer un compte ↗135 </a>136 </div>137 </div>138 );139 }140141 return (142 <div className="container profil">143 <span className="kicker">Mon compte</span>144 <h1>145 {me.name ? <>Salut, <span className="hl">{me.name.split(" ")[0]}</span>.</>146 : <>Votre <span className="hl">profil</span>.</>}147 </h1>148149 {/* ——— Carte de membre Groupe KA ——— */}150 <div className="pc" role="img" aria-label={`Carte de membre ${me.ka_id}`}>151 <div className="pc-watermark" aria-hidden="true">KA</div>152 <div className="pc-head">153 <span className="pc-brand">Groupe <span className="pc-ka">KA</span></span>154 <span className="pc-label">Carte de membre · Groupe KA</span>155 </div>156 <div className="pc-id-block">157 <span className="pc-id-label">KA-ID</span>158 <span className="pc-id">{me.ka_id}</span>159 </div>160 <div className="pc-foot">161 <div className="pc-holder">162 <span className="pc-holder-name">{me.name || me.email}</span>163 {me.role_label && (164 <span className="pc-role-badge">{me.role_label}</span>165 )}166 <span className="pc-holder-since">Membre depuis le {fmtEpoch(me.created_at)}</span>167 </div>168 {me.picture && (169 <img className="pc-avatar" src={me.picture} alt="" referrerPolicy="no-referrer" />170 )}171 </div>172 <div className="pc-strip" aria-hidden="true">173 {Array.from({ length: 28 }).map((_, i) => <i key={i} />)}174 </div>175 </div>176177 <button className={`btn btn-ghost pc-copy ${copied ? "ok" : ""}`} onClick={copyKaId}>178 {copied ? "✓ Copié" : "Copier mon KA-ID"}179 </button>180181 {/* ——— Informations ——— */}182 <section className="profil-grid">183 <div className="pg-item">184 <span className="pg-label">Nom</span>185 <span className="pg-value">{me.name || "—"}</span>186 </div>187 <div className="pg-item">188 <span className="pg-label">Courriel</span>189 <span className="pg-value">{me.email || "—"}</span>190 </div>191 <div className="pg-item">192 <span className="pg-label">Identifiant membre</span>193 <span className="pg-value mono">{me.ka_id}</span>194 </div>195 <div className="pg-item">196 <span className="pg-label">Connexion</span>197 <span className="pg-value">{providerLabel(me.provider)}</span>198 </div>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 <p className="profil-note">212 Votre <b>KA-ID</b> est votre identifiant unique dans l'écosystème{" "}213 <a href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer">214 Groupe KA215 </a>{" "}216 — le même sur toutes les plateformes du groupe. Food·Ka ne conserve que217 les informations de ce profil ; rien d'autre, et jamais revendues.218 </p>219220 {/* ——— Actions ——— */}221 <div className="profil-actions">222 <button223 className="btn btn-ghost"224 onClick={async () => { await logout(); nav("/"); window.location.reload(); }}225 >226 Se déconnecter227 </button>228 <button229 className="btn btn-ghost"230 onClick={() => window.dispatchEvent(new Event("foodka:openConsent"))}231 >232 Gérer mes témoins233 </button>234 <Link className="btn btn-ghost" to="/confidentialite">Confidentialité</Link>235 </div>236 </div>237 );238}239