Repository Public
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2/**3 * Mon compte — carte de membre Groupe KA (gabarit « pc » de Lou·Ka transposé) :4 * fond encre, filigrane KA, KA-ID en gros mono lime (#d9f26b) avec glow,5 * porteur + avatar + badge statut, bande code-barres ; profil Groupe KA en6 * lecture seule (saisi sur groupe-ka.com/compte, LA source de vérité), grille7 * d'infos et actions dessous.8 * Le lime « Groupe KA » est propre à la carte (l'accent Vrai-Prix reste rouge).9 */10"use client";11import { useState } from "react";12import { useRouter } from "next/navigation";13import Link from "next/link";14import { useLang } from "@/components/LangContext";15import type { HubProfile, HubSocials } from "@/lib/hub-profile";1617// couleur signature du Groupe KA — réservée à la carte de membre18const KA_LIME = "#d9f26b";1920type User = {21 ka_id: string;22 email: string;23 name: string;24 picture: string;25 provider: string;26};2728// réseaux affichés en chips — libellé + URL depuis le pseudo (« @x ») ou l'URL29const SOCIAL_LABELS: Record<keyof HubSocials, string> = {30 instagram: "Instagram",31 facebook: "Facebook",32 x: "X",33 linkedin: "LinkedIn",34 tiktok: "TikTok",35 youtube: "YouTube",36};3738function socialUrl(net: keyof HubSocials, v: string): string {39 if (/^https?:\/\//i.test(v)) return v;40 const h = v.replace(/^@/, "");41 switch (net) {42 case "instagram":43 return `https://instagram.com/${h}`;44 case "facebook":45 return `https://facebook.com/${h}`;46 case "x":47 return `https://x.com/${h}`;48 case "linkedin":49 return `https://linkedin.com/in/${h}`;50 case "tiktok":51 return `https://tiktok.com/@${h}`;52 case "youtube":53 return `https://youtube.com/@${h}`;54 }55}5657export default function CompteClient({58 user,59 profile,60}: {61 user: User;62 profile: HubProfile | null;63}) {64 const { lang } = useLang();65 const fr = lang === "fr";66 const router = useRouter();67 const [copied, setCopied] = useState(false);68 const [out, setOut] = useState(false);6970 const copyKaId = async () => {71 try {72 await navigator.clipboard.writeText(user.ka_id);73 setCopied(true);74 setTimeout(() => setCopied(false), 1800);75 } catch {76 /* presse-papiers indisponible : tant pis */77 }78 };7980 const logout = async () => {81 setOut(true);82 try {83 await fetch("/api/auth/logout", { method: "POST" });84 } finally {85 router.push("/");86 router.refresh();87 }88 };8990 const infos: [string, React.ReactNode][] = [91 [fr ? "Nom" : "Name", user.name || "—"],92 [fr ? "Courriel" : "Email", user.email || "—"],93 [94 "KA-ID",95 <span key="k" className="vp-mono">{user.ka_id}</span>,96 ],97 [98 fr ? "Connexion" : "Sign-in",99 user.provider === "ka-id" || !user.provider100 ? "KA ID (groupe-ka.com)"101 : `KA ID · ${user.provider}`,102 ],103 ];104105 return (106 <div className="mx-auto max-w-2xl pb-10 pt-10">107 <span className="kicker">{fr ? "Mon compte" : "My account"}</span>108 <h1 className="mb-7 mt-2 text-[clamp(30px,5vw,44px)] font-bold leading-tight">109 {user.name ? (110 <>111 {fr ? "Salut, " : "Hi, "}112 <span className="hl">{user.name.split(" ")[0]}</span>.113 </>114 ) : (115 <>116 {fr ? "Votre " : "Your "}117 <span className="hl">{fr ? "profil" : "profile"}</span>.118 </>119 )}120 </h1>121122 {/* ——— Carte de membre Groupe KA ——— */}123 <div124 role="img"125 aria-label={`${fr ? "Carte de membre" : "Member card"} ${user.ka_id}`}126 className="relative max-w-[520px] overflow-hidden rounded-2xl border-2 border-ink bg-ink px-[26px] pt-6 text-paper"127 style={{ boxShadow: "10px 10px 0 rgba(20, 24, 20, 0.18)" }}128 >129 {/* filigrane */}130 <div131 aria-hidden="true"132 className="vp-display pointer-events-none absolute -top-[34px] right-[-18px] rotate-[-8deg] text-[170px] font-bold leading-none tracking-[-0.06em]"133 style={{ color: "rgba(217, 242, 107, 0.07)" }}134 >135 KA136 </div>137138 {/* tête : marque + libellé */}139 <div className="flex flex-wrap items-baseline justify-between gap-3">140 <span className="vp-display text-[24px] font-bold tracking-[-0.04em]">141 Groupe142 <span143 className="ml-[3px] inline-block rotate-[-2deg] rounded-[5px] px-[6px] pb-[3px] pt-[1px] text-ink"144 style={{ background: KA_LIME }}145 >146 KA147 </span>148 </span>149 <span className="vp-mono text-[10px] uppercase tracking-[0.16em] text-[rgba(245,243,238,0.55)]">150 {fr ? "Carte de membre · Groupe KA" : "Member card · Groupe KA"}151 </span>152 </div>153154 {/* KA-ID */}155 <div className="my-[24px] flex flex-col gap-1">156 <span157 className="vp-mono text-[10px] uppercase tracking-[0.2em]"158 style={{ color: "rgba(217, 242, 107, 0.65)" }}159 >160 KA-ID161 </span>162 <span163 className="vp-mono text-[clamp(22px,6vw,34px)] font-bold tracking-[0.14em]"164 style={{ color: KA_LIME, textShadow: "0 0 24px rgba(217, 242, 107, 0.35)" }}165 >166 {user.ka_id}167 </span>168 </div>169170 {/* porteur + avatar */}171 <div className="flex items-end justify-between gap-3.5 pb-[18px]">172 <div className="flex min-w-0 flex-col gap-0.5">173 <span className="flex min-w-0 items-center gap-2">174 <span className="truncate text-[15px] font-semibold">175 {user.name || user.email}176 </span>177 {profile?.role_label ? (178 // badge statut — vient du hub (role_label), lime comme la carte179 <span180 className="vp-mono flex-none rounded-[4px] border px-[7px] pb-[2px] pt-[1px] text-[9.5px] font-bold uppercase tracking-[0.08em]"181 style={{182 borderColor: "rgba(217, 242, 107, 0.55)",183 color: KA_LIME,184 background: "rgba(217, 242, 107, 0.1)",185 }}186 >187 {profile.role_label}188 </span>189 ) : null}190 </span>191 <span className="vp-mono text-[10.5px] tracking-[0.06em] text-[rgba(245,243,238,0.5)]">192 {fr193 ? "Valide sur toutes les plateformes du Groupe KA"194 : "Valid across all Groupe KA platforms"}195 </span>196 </div>197 {user.picture ? (198 // eslint-disable-next-line @next/next/no-img-element199 <img200 src={user.picture}201 alt=""202 referrerPolicy="no-referrer"203 className="h-[52px] w-[52px] flex-none rounded-full border-2 object-cover"204 style={{ borderColor: KA_LIME }}205 />206 ) : (207 <span208 className="vp-display flex h-[52px] w-[52px] flex-none items-center justify-center rounded-full border-2 text-[22px] font-bold"209 style={{ borderColor: KA_LIME, color: KA_LIME }}210 >211 {(user.name || user.email || "K").charAt(0).toUpperCase()}212 </span>213 )}214 </div>215216 {/* bande code-barres */}217 <div218 aria-hidden="true"219 className="-mx-[26px] flex gap-[5px] overflow-hidden px-[18px] py-[9px]"220 style={{221 background: "rgba(217, 242, 107, 0.1)",222 borderTop: "1px solid rgba(217, 242, 107, 0.25)",223 }}224 >225 {Array.from({ length: 34 }).map((_, i) => (226 <i227 key={i}228 className="block w-[3px] rounded-[1px]"229 style={{230 background: KA_LIME,231 height: i % 3 === 0 ? 14 : i % 3 === 1 ? 9 : 17,232 opacity: i % 3 === 0 ? 0.35 : i % 3 === 1 ? 0.7 : 0.9,233 }}234 />235 ))}236 </div>237 </div>238239 {/* ——— actions carte ——— */}240 <div className="mt-4 flex flex-wrap gap-2.5">241 <button242 className="btn btn-ghost"243 onClick={copyKaId}244 style={copied ? { background: KA_LIME, color: "var(--ink)" } : undefined}245 >246 {copied ? "✓ " + (fr ? "Copié" : "Copied") : fr ? "Copier mon KA-ID" : "Copy my KA-ID"}247 </button>248 <Link href="/favoris" className="btn btn-ghost">249 ♥ {fr ? "Mes favoris" : "My favourites"}250 </Link>251 <button className="btn btn-ghost" onClick={logout} disabled={out}>252 {out ? (fr ? "Déconnexion…" : "Signing out…") : fr ? "Se déconnecter" : "Sign out"}253 </button>254 </div>255256 {/* ——— Mon profil Groupe KA — lecture seule, saisi sur groupe-ka.com ——— */}257 <section className="vp-card mt-8 p-5 sm:p-6">258 <span className="kicker">259 {fr ? "Mon profil Groupe KA" : "My Groupe KA profile"}260 </span>261262 {profile ? (263 <>264 {profile.bio ? (265 <p className="mt-3 max-w-xl text-[14.5px] leading-relaxed">266 {profile.bio}267 </p>268 ) : null}269270 <div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">271 {profile.job_title || profile.company ? (272 <div className="kv-cell">273 <div className="k">{fr ? "Emploi" : "Job"}</div>274 <div className="v break-words">275 {[profile.job_title, profile.company]276 .filter(Boolean)277 .join(" · ")}278 </div>279 </div>280 ) : null}281 {profile.city ? (282 <div className="kv-cell">283 <div className="k">{fr ? "Ville" : "City"}</div>284 <div className="v break-words">{profile.city}</div>285 </div>286 ) : null}287 {profile.age != null ? (288 <div className="kv-cell">289 <div className="k">{fr ? "Âge" : "Age"}</div>290 <div className="v">291 {profile.age} {fr ? "ans" : "yrs"}292 </div>293 </div>294 ) : null}295 {profile.website ? (296 <div className="kv-cell">297 <div className="k">{fr ? "Site web" : "Website"}</div>298 <div className="v break-words">299 <a300 href={301 /^https?:\/\//i.test(profile.website)302 ? profile.website303 : `https://${profile.website}`304 }305 target="_blank"306 rel="noopener noreferrer"307 className="underline decoration-2 underline-offset-2"308 >309 {profile.website.replace(/^https?:\/\//i, "")}310 </a>311 </div>312 </div>313 ) : null}314 <div className="kv-cell">315 <div className="k">{fr ? "Statut" : "Status"}</div>316 <div className="v break-words">317 {profile.role_label || (fr ? "Membre" : "Member")}318 {profile.public && profile.public_url ? (319 <>320 {" · "}321 <a322 href={profile.public_url}323 target="_blank"324 rel="noopener noreferrer"325 className="underline decoration-2 underline-offset-2"326 >327 {fr ? "profil public ↗" : "public profile ↗"}328 </a>329 </>330 ) : null}331 </div>332 </div>333 </div>334335 {/* réseaux sociaux — chips cliquables */}336 {Object.entries(profile.socials || {}).some(([, v]) => v) ? (337 <div className="mt-4 flex flex-wrap gap-2">338 {(Object.keys(SOCIAL_LABELS) as (keyof HubSocials)[]).map(339 (net) => {340 const v = profile.socials?.[net];341 if (!v) return null;342 return (343 <a344 key={net}345 href={socialUrl(net, v)}346 target="_blank"347 rel="noopener noreferrer"348 className="stat-chip transition-colors hover:text-accent-deep"349 >350 <b>{SOCIAL_LABELS[net]}</b> {v}351 </a>352 );353 },354 )}355 </div>356 ) : null}357 </>358 ) : (359 <p className="mt-3 max-w-xl text-[13.5px] leading-relaxed text-ink-2">360 {fr361 ? "Votre profil se remplit sur le hub groupe-ka.com — bio, ville, emploi, réseaux sociaux…"362 : "Your profile is filled in on the groupe-ka.com hub — bio, city, job, social networks…"}363 </p>364 )}365366 <div className="mt-5">367 <a368 href="https://www.groupe-ka.com/compte"369 target="_blank"370 rel="noopener noreferrer"371 className="btn btn-primary"372 >373 {fr374 ? "Modifier mon profil sur groupe-ka.com ↗"375 : "Edit my profile on groupe-ka.com ↗"}376 </a>377 </div>378 <p className="mt-3 text-[12.5px] text-ink-2">379 {fr380 ? "Une seule saisie, visible sur les huit plateformes du groupe."381 : "Enter it once — visible across the group's eight platforms."}382 </p>383 </section>384385 {/* ——— informations ——— */}386 <section className="mt-8 grid grid-cols-1 gap-3 sm:grid-cols-2">387 {infos.map(([k, v]) => (388 <div key={k} className="kv-cell">389 <div className="k">{k}</div>390 <div className="v break-words">{v}</div>391 </div>392 ))}393 </section>394395 <p className="mt-7 max-w-xl text-[13.5px] leading-relaxed text-ink-2">396 {fr ? (397 <>398 Votre <b>KA-ID</b> vous suit sur toutes les plateformes du{" "}399 <a400 href="https://www.groupe-ka.com"401 target="_blank"402 rel="noopener noreferrer"403 className="font-bold underline decoration-2 underline-offset-2"404 >405 Groupe KA406 </a>{" "}407 — c'est le même identifiant partout. Vrai-Prix ne conserve que408 cette session (nom, courriel, photo) ; rien d'autre, et jamais409 revendu.410 </>411 ) : (412 <>413 Your <b>KA-ID</b> follows you across every{" "}414 <a415 href="https://www.groupe-ka.com"416 target="_blank"417 rel="noopener noreferrer"418 className="font-bold underline decoration-2 underline-offset-2"419 >420 Groupe KA421 </a>{" "}422 platform — the same ID everywhere. Vrai-Prix only keeps this session423 (name, email, picture); nothing else, never sold.424 </>425 )}426 </p>427 </div>428 );429}430