SPB Git

Repository Public

CompteClient.tsx 15.1 KB 430 lines tsx

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&apos;est le même identifiant partout. Vrai-Prix ne conserve que408            cette session (nom, courriel, photo) ; rien d&apos;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
Files main
Menu

Theme