Agrégateur de produits québécois — www.fabri-ka.com
Python 38.4%
HTML 30.3%
TypeScript 17.2%
CSS 11%
JavaScript 3.2%
1// -----------------------------------------------------------------------------2// Fabri-Ka — Agrégateur de produits québécois3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// pages/Profile.tsx : profil utilisateur — carte de membre Groupe KA (KA-ID),5// profil Groupe KA en lecture seule (bio, emploi, ville, âge, site, réseaux6// sociaux — géré sur groupe-ka.com), informations du compte, actions7// -----------------------------------------------------------------------------8import { useEffect, useState } from 'react'9import { useNavigate } from 'react-router-dom'10import { fetchMe, logout, type Me, type Socials } from '../api'1112const fmtEpoch = (ts: number | null | undefined): string => {13 if (!ts) return '—'14 return new Date(ts * 1000)15 .toLocaleDateString('fr-CA', { day: 'numeric', month: 'long', year: 'numeric' })16 .replace(/^1 /, '1ᵉʳ ')17}1819const SOCIAL_BASE: Record<string, string> = {20 instagram: 'https://www.instagram.com/',21 facebook: 'https://www.facebook.com/',22 x: 'https://x.com/',23 linkedin: 'https://www.linkedin.com/in/',24 tiktok: 'https://www.tiktok.com/@',25 youtube: 'https://www.youtube.com/@',26}2728const SOCIAL_LABELS: { key: keyof Socials; label: string }[] = [29 { key: 'instagram', label: 'Instagram' },30 { key: 'facebook', label: 'Facebook' },31 { key: 'x', label: 'X (Twitter)' },32 { key: 'linkedin', label: 'LinkedIn' },33 { key: 'tiktok', label: 'TikTok' },34 { key: 'youtube', label: 'YouTube' },35]3637/** « @pseudo » ou « pseudo » -> URL complète de la plateforme ; URL laissée telle quelle */38function socialUrl(key: string, value: string): string {39 const v = value.trim()40 if (/^https?:\/\//i.test(v)) return v41 if (key === 'website') return `https://${v}`42 return (SOCIAL_BASE[key] ?? 'https://') + v.replace(/^@/, '')43}4445/** Profil géré au HUB Groupe KA — LECTURE SEULE (l'édition se fait sur46 * groupe-ka.com/compte, une seule saisie pour les huit plateformes). */47function HubProfileSection({ me }: { me: Me }) {48 const socials = SOCIAL_LABELS.filter((s) => (me.socials?.[s.key] ?? '').trim())49 return (50 <section className="hub-profile">51 <h2>Mon profil Groupe KA</h2>52 {me.bio && <p className="hub-bio">{me.bio}</p>}53 <div className="hub-chips">54 {(me.job_title || me.company) && (55 <span className="hub-chip">56 {[me.job_title, me.company].filter(Boolean).join(' · ')}57 </span>58 )}59 {me.city && <span className="hub-chip">{me.city}</span>}60 {me.age != null && <span className="hub-chip">{me.age} ans</span>}61 {me.website && (62 <a className="hub-chip hub-chip-link" href={socialUrl('website', me.website)}63 target="_blank" rel="noopener noreferrer">Site web ↗</a>64 )}65 {socials.map((s) => (66 <a key={s.key} className="hub-chip hub-chip-link"67 href={socialUrl(s.key, me.socials![s.key]!)}68 target="_blank" rel="noopener noreferrer" title={s.label}>69 {s.label} ↗70 </a>71 ))}72 </div>73 <div className="hub-actions">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 les huit plateformes du groupe.83 </p>84 </section>85 )86}8788export default function Profile() {89 const [me, setMe] = useState<Me | null | undefined>(undefined) // undefined = chargement90 const [copied, setCopied] = useState(false)91 const navigate = useNavigate()9293 useEffect(() => {94 fetchMe()95 .then((r) => setMe(r.user))96 .catch(() => setMe(null))97 }, [])9899 const copyKaId = async () => {100 if (!me?.ka_id) return101 try {102 await navigator.clipboard.writeText(me.ka_id)103 setCopied(true)104 setTimeout(() => setCopied(false), 1800)105 } catch {106 /* presse-papiers indisponible : tant pis */107 }108 }109110 if (me === undefined) {111 return (112 <div className="page profil">113 <p className="profil-loading">Chargement…</p>114 </div>115 )116 }117118 if (me === null) {119 return (120 <div className="page profil">121 <h1>Mon profil</h1>122 <p className="profil-lede">123 Connectez-vous avec votre compte <strong>KA ID</strong> — le même124 identifiant de membre sur toutes les plateformes du Groupe KA.125 </p>126 <a className="btn btn-primary" href="/api/auth/ka/login?next=%2Fprofil">127 Se connecter avec KA ID128 </a>129 </div>130 )131 }132133 return (134 <div className="page profil">135 <h1>136 {me.name ? <>Salut, {me.name.split(' ')[0]}.</> : <>Mon profil</>}137 </h1>138139 {/* ——— Carte de membre Groupe KA ——— */}140 <div className="pc" role="img" aria-label={`Carte de membre ${me.ka_id}`}>141 <div className="pc-watermark" aria-hidden="true">KA</div>142 <div className="pc-head">143 <span className="pc-brand">144 Groupe <span className="pc-ka">KA</span>145 </span>146 <span className="pc-label">Carte de membre · Groupe KA</span>147 </div>148 <div className="pc-id-block">149 <span className="pc-id-label">KA-ID</span>150 <span className="pc-id">{me.ka_id}</span>151 </div>152 <div className="pc-foot">153 <div className="pc-holder">154 <span className="pc-holder-name">{me.name || me.email}</span>155 {me.role_label && (156 <span className="pc-role-badge">{me.role_label}</span>157 )}158 <span className="pc-holder-since">159 Membre depuis le {fmtEpoch(me.created)}160 </span>161 </div>162 {me.picture && (163 <img className="pc-avatar" src={me.picture} alt="" referrerPolicy="no-referrer" />164 )}165 </div>166 <div className="pc-strip" aria-hidden="true">167 {Array.from({ length: 28 }).map((_, i) => (168 <i key={i} />169 ))}170 </div>171 </div>172173 <button className={`btn btn-secondary pc-copy ${copied ? 'ok' : ''}`} onClick={copyKaId}>174 {copied ? '✓ Copié' : 'Copier mon KA-ID'}175 </button>176177 {/* ——— Profil Groupe KA (lecture seule, géré sur groupe-ka.com) ——— */}178 {me.profile_source === 'groupe-ka' && <HubProfileSection me={me} />}179180 {/* ——— Informations ——— */}181 <section className="profil-grid">182 <div className="pg-item">183 <span className="pg-label">Nom</span>184 <span className="pg-value">{me.name || '—'}</span>185 </div>186 <div className="pg-item">187 <span className="pg-label">Courriel</span>188 <span className="pg-value">{me.email || '—'}</span>189 </div>190 <div className="pg-item">191 <span className="pg-label">Identifiant membre</span>192 <span className="pg-value mono">{me.ka_id}</span>193 </div>194 <div className="pg-item">195 <span className="pg-label">Connexion</span>196 <span className="pg-value">197 {me.provider === 'google' ? 'Compte Google (via KA ID)' : 'KA ID (groupe-ka.com)'}198 </span>199 </div>200 <div className="pg-item">201 <span className="pg-label">Membre depuis</span>202 <span className="pg-value">{fmtEpoch(me.created)}</span>203 </div>204 <div className="pg-item">205 <span className="pg-label">Dernière connexion</span>206 <span className="pg-value">{fmtEpoch(me.last_login)}</span>207 </div>208 </section>209210 <p className="profil-note">211 Votre <strong>KA-ID</strong> est votre identifiant unique dans212 l'écosystème{' '}213 <a href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer">214 Groupe KA215 </a>{' '}216 — il vous suit sur toutes les plateformes du groupe. Fabri-Ka ne217 conserve que les informations de ce profil ; rien d'autre, et jamais218 revendues.219 </p>220221 {/* ——— Actions ——— */}222 <div className="profil-actions">223 <button224 className="btn btn-secondary"225 onClick={async () => {226 await logout()227 navigate('/')228 window.location.reload()229 }}230 >231 Se déconnecter232 </button>233 </div>234 </div>235 )236}237