Concorde
BETA

UserProfileCard

Tarjeta de zona de usuario (766×140): saludo, riesgo del perfil, puntos VMC y accesos. Compone AvatarZone, ProfileButton y StarIcon.

Avatar de ZAEX5G¡Hola, ZAEX5G!
RIESGO DEL PERFILMuy bajo
PUNTOS VMC500 ptsMás información sobre los puntos VMC
<UserProfileCard username="ZAEX5G" />

Instalación

$ npx github:AaronCoorahua/ConcordeV2#cli add userprofilecard

Uso

import UserProfileCard from "@/src/components/UserProfileCard";

<UserProfileCard username="ZAEX5G" />

Ejemplos

Por defecto

Saludo, riesgo del perfil, puntos VMC y accesos a perfil / historial.

Avatar de ZAEX5G¡Hola, ZAEX5G!
RIESGO DEL PERFILMuy bajo
PUNTOS VMC500 ptsMás información sobre los puntos VMC
<UserProfileCard />

Datos personalizados

Todos los textos son props: usuario, riesgo y puntos.

Avatar de MROA21¡Hola, MROA21!
RIESGO DEL PERFILMedio
PUNTOS VMC1 240 ptsMás información sobre los puntos VMC
<UserProfileCard
  username="MROA21"
  riskValue="Medio"
  points="1 240 pts"
/>

API

PropTipoDefault
username
Usuario mostrado en «¡Hola, {username}!».
string"ZAEX5G"
riskLabel
Etiqueta de la 1ª estadística.
string"RIESGO DEL PERFIL"
riskValue
Valor de riesgo (naranja).
string"Muy bajo"
pointsLabel
Etiqueta de la 2ª estadística.
string"PUNTOS VMC"
points
Puntos VMC junto a la estrella.
string"500 pts"
onProfile
Click del acceso «Tu perfil».
MouseEventHandler
profileHref
Si se pasa, «Tu perfil» se renderiza como enlace.
string
onHistory
Click del acceso «Historial».
MouseEventHandler
historyHref
Si se pasa, «Historial» se renderiza como enlace.
string
className
Clases extra sobre la tarjeta.
string

Código del componente

Ver UserProfileCard.tsx completo
UserProfileCard.tsx
"use client";

/**
 * UserProfileCard — Generado por Concorde
 * Fuente: Figma VOYAGER · "UserProfileCard" (nodo 2035:16581)
 *
 * Tarjeta de zona de usuario 766×140 (blanca, radius 16, drop shadow suave).
 * Dos filas internas de 734 de ancho separadas por un divisor #E1E3E2:
 *   1ª (52px): avatar (AvatarZone) + «¡Hola, {usuario}!»  ·······  «Tu perfil ›» (ProfileButton)
 *   2ª (40px): RIESGO DEL PERFIL / valor  +  PUNTOS VMC / ⭐ pts ⓘ  ···  «Historial ›» (ProfileButton)
 *
 * Figma manda en lo visual; Concorde compone los componentes existentes
 * (AvatarZone, ProfileButton, StarIcon) y aporta el layout + accesibilidad.
 */

import type { JSX, MouseEventHandler } from "react";
import AvatarZone from "@/src/components/AvatarZone";
import ProfileButton from "@/src/components/ProfileButton";
import StarIcon from "@/src/components/StarIcon";
import InfoIcon from "@/src/components/InfoIcon";

// ─── Types ────────────────────────────────────────────────────────────────────

export interface UserProfileCardProps {
  /** Usuario mostrado en «¡Hola, {usuario}!» (default "ZAEX5G"). */
  username?: string;
  /** Etiqueta de la 1ª estadística (default "RIESGO DEL PERFIL"). */
  riskLabel?: string;
  /** Valor de riesgo (default "Muy bajo"). */
  riskValue?: string;
  /** Etiqueta de la 2ª estadística (default "PUNTOS VMC"). */
  pointsLabel?: string;
  /** Puntos VMC (default "500 pts"). */
  points?: string;
  /** Acción / enlace del acceso «Tu perfil». */
  onProfile?: MouseEventHandler<HTMLElement>;
  profileHref?: string;
  /** Acción / enlace del acceso «Historial». */
  onHistory?: MouseEventHandler<HTMLElement>;
  historyHref?: string;
  className?: string;
}

// ─── Self-contained CSS ───────────────────────────────────────────────────────

const STYLE_ID = "concorde-userprofilecard-styles";

const STYLES = `
.upc {
  box-sizing: border-box;
  width: 766px;
  height: 140px;
  padding: 16px;
  border-radius: 16px;
  background: #ffffff;
  /* Drop shadow del filtro Figma (dilate 4 · blur 8 · negro 10%) */
  box-shadow: 0 0 8px 4px rgba(0,0,0,0.08);
  font-family: var(--vmc-font-display, "Plus Jakarta Sans", -apple-system, sans-serif);
}
.upc__inner {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  height: 100%;
}
/* ── Fila 1 (734×52) ── */
.upc__row1 {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 52px;
}
.upc__greet {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.upc__hello {
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 24px;
  white-space: nowrap;
  /* Texto con gradiente vault del Figma (paint1: #5F3ED8 → #340091 → #140046) */
  background: linear-gradient(168deg, #5F3ED8 0%, #340091 50%, #140046 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* ── Divisor ── */
.upc__divider {
  height: 1px;
  background: #E1E3E2;
}
/* ── Fila 2 (734×40) ── */
.upc__row2 {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 40px;
}
.upc__stats {
  display: flex;
  align-items: center;
  gap: 56px;
}
.upc__stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.upc__stat-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #99A1AF;
  white-space: nowrap;
}
.upc__stat-value {
  font-size: 14px;
  font-weight: 700;
  line-height: 18px;
  white-space: nowrap;
}
.upc__stat-value--risk { color: #ED8936; }
.upc__points {
  display: flex;
  align-items: center;
  gap: 6px;
}
.upc__points-text { color: #3B1782; }
.upc__info { display: block; flex-shrink: 0; }
`;

let _stylesInjected = false;

// ─── Component ────────────────────────────────────────────────────────────────

export default function UserProfileCard({
  username = "ZAEX5G",
  riskLabel = "RIESGO DEL PERFIL",
  riskValue = "Muy bajo",
  pointsLabel = "PUNTOS VMC",
  points = "500 pts",
  onProfile,
  profileHref,
  onHistory,
  historyHref,
  className = "",
}: UserProfileCardProps): JSX.Element {
  if (typeof document !== "undefined" && !_stylesInjected) {
    if (!document.getElementById(STYLE_ID)) {
      const el = document.createElement("style");
      el.id = STYLE_ID;
      el.textContent = STYLES;
      document.head.appendChild(el);
    }
    _stylesInjected = true;
  }

  return (
    <>
      <style id={`${STYLE_ID}-ssr`} suppressHydrationWarning dangerouslySetInnerHTML={{ __html: STYLES }} />
      <section className={["upc", className].filter(Boolean).join(" ")} data-component="user-profile-card">
        <div className="upc__inner">
          {/* Fila 1 — saludo + acceso al perfil */}
          <div className="upc__row1">
            <div className="upc__greet">
              <AvatarZone size="md" title={`Avatar de ${username}`} />
              <span className="upc__hello">{`¡Hola, ${username}!`}</span>
            </div>
            <ProfileButton href={profileHref} onClick={onProfile} aria-label="Tu perfil">
              Tu perfil
            </ProfileButton>
          </div>

          {/* Divisor */}
          <div className="upc__divider" role="separator" aria-hidden="true" />

          {/* Fila 2 — estadísticas + historial */}
          <div className="upc__row2">
            <div className="upc__stats">
              <div className="upc__stat">
                <span className="upc__stat-label">{riskLabel}</span>
                <span className="upc__stat-value upc__stat-value--risk">{riskValue}</span>
              </div>
              <div className="upc__stat">
                <span className="upc__stat-label">{pointsLabel}</span>
                <span className="upc__points">
                  {/* StarIcon usa viewBox 28 con padding de sombra: el badge ocupa
                      16/28 → size 24 lo deja en ~16px, igual que el InfoIcon (16). */}
                  <StarIcon size={24} />
                  <span className="upc__stat-value upc__points-text">{points}</span>
                  <InfoIcon className="upc__info" title="Más información sobre los puntos VMC" />
                </span>
              </div>
            </div>
            <ProfileButton href={historyHref} onClick={onHistory} aria-label="Historial">
              Historial
            </ProfileButton>
          </div>
        </div>
      </section>
    </>
  );
}