SPB Git forge

spb/fabri-ka

Public

Agrégateur de produits québécois — www.fabri-ka.com

217commits 1branches 0releases
66.1 MBsize
maindefault branch
5 h agolast push
Python 38.4% HTML 30.3% TypeScript 17.2% CSS 11% JavaScript 3.2%
8.3 KB · 237 lines tsx
Raw Blame History
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