Perfil

La sección Perfil de los ajustes de un producto de IA. Recorta una foto en un círculo, elige un nombre de usuario que se comprueba mientras escribes, confirma un nuevo correo con un código de 6 dígitos, añade enlaces que reconocen el sitio y mira una tarjeta en vivo de cómo te ven los demás.

ProfileSettings es la página de perfil de un producto como Claude o Cursor, hecha para integrarse en cualquier sección de SettingsShell. Dale el perfil guardado y un onSave, y se encarga del resto: un borrador que solo se guarda cuando tú lo dices, errores de campo desde tus comprobaciones o tu servidor, y la barra de guardado flotante de Settings con ⌘S.

Arriba, una tarjeta muestra cómo ven los demás tu perfil y cambia mientras escribes: foto, nombre, handle, biografía y enlaces. Desactiva Show my profile y la tarjeta se atenúa e indica que solo tú puedes verla. Copy link copia la URL del perfil en el portapapeles.

Elige una foto, o suéltala sobre la fila, y se abre en un diálogo de recorte. Arrastra para moverla, haz zoom con el control deslizante, la rueda o un pellizco en el trackpad, y la foto se recorta a un cuadrado en el navegador antes de llegar a onSave como URL de datos. Los archivos de tipo incorrecto, demasiado grandes o de menos de 128 píxeles se rechazan en la fila con el motivo.

El nombre de usuario se comprueba mientras escribes, sin ralentizar la escritura. Las comprobaciones esperan 350ms tras la última tecla, las obsoletas se cancelan mediante un AbortSignal, y los nombres que ya comprobaste responden al instante. Un nombre ocupado muestra tus sugerencias como correcciones de un toque. Si guardas antes de que termine una comprobación, el guardado la espera.

El correo no forma parte del borrador. Change abre un diálogo de dos pasos: introduce la nueva dirección y luego el código de 6 dígitos enviado a ella, que se comprueba solo cuando entra el último dígito. Un código incorrecto sacude y se borra, Resend se desbloquea tras 30 segundos y la fila se actualiza una vez confirmado el código. Una dirección sin verificar muestra Resend link.

Los enlaces aceptan lo que la gente pegue: github.com/you, una URL completa o una dirección www. Los sitios conocidos obtienen su icono, cualquier otro su primera letra. Al guardar se normalizan a URLs https y los vacíos se descartan. Las direcciones que no son enlaces o repiten otra se marcan una por una.

  1. Añade el registro Pro a components.json

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Añade tu token

    Crea un token en tu página de cuenta y colócalo en .env.local como HEXTAUI_PRO_TOKEN.

  3. Añade el bloque

    pnpm dlx shadcn@latest add @hextaui-pro/profile

Conéctalo a tu API

Pasa el perfil guardado y un onSave que devuelva errores de campo o lance un error. checkUsername recibe un AbortSignal, así que las comprobaciones obsoletas se cancelan mientras la gente escribe.

"use client"

import { SettingsSection, SettingsShell } from "../settings/settings"

import { ProfileSettings, type ProfileValues } from "@/components/blocks/profile/profile"

export function ProfilePage({ profile, email }: { profile: ProfileValues; email: string }) {
  return (
    <SettingsShell sections={[{ id: "profile", label: "Profile" }]} className="h-svh">
      <SettingsSection id="profile">
        <ProfileSettings
          profile={profile}
          email={email}
          checkUsername={async (username, signal) => {
            const response = await fetch(`/api/usernames/${username}`, { signal })
            return response.json()
          }}
          onSave={async (values) => {
            const response = await fetch("/api/profile", {
              method: "PATCH",
              body: JSON.stringify(values),
            })
            if (response.status === 409) return { username: "That username is taken." }
            if (!response.ok) throw new Error("Check your connection and try again.")
          }}
        />
      </SettingsSection>
    </SettingsShell>
  )
}

Cambiar y verificar el correo

Añade onRequestEmailCode y onVerifyEmailCode para activar el botón Change. Devuelve un mensaje desde onRequestEmailCode para rechazar una dirección, y false desde onVerifyEmailCode para un código incorrecto.

"use client"

import * as React from "react"

import { SettingsSection, SettingsShell } from "../settings/settings"

import { ProfileSettings, type ProfileValues } from "@/components/blocks/profile/profile"

export function ProfileWithEmail({ profile, account }: {
  profile: ProfileValues
  account: { email: string; verified: boolean }
}) {
  const [email, setEmail] = React.useState(account.email)

  return (
    <SettingsShell sections={[{ id: "profile", label: "Profile" }]} className="h-svh">
      <SettingsSection id="profile">
        <ProfileSettings
          profile={profile}
          email={email}
          emailVerified={account.verified}
          onSave={(values) => fetch("/api/profile", { method: "PATCH", body: JSON.stringify(values) }).then(() => {})}
          onResendVerification={() => fetch("/api/email/verify", { method: "POST" })}
          onRequestEmailCode={async (next) => {
            const response = await fetch("/api/email/change", {
              method: "POST",
              body: JSON.stringify({ email: next }),
            })
            if (response.status === 409) return "That address belongs to another account."
            if (!response.ok) throw new Error("Couldn’t send the code. Try again.")
          }}
          onVerifyEmailCode={async (next, code) => {
            const response = await fetch("/api/email/change/verify", {
              method: "POST",
              body: JSON.stringify({ email: next, code }),
            })
            return response.ok
          }}
          onEmailChange={setEmail}
        />
      </SettingsSection>
    </SettingsShell>
  )
}

Anatomía

Las partes que compones, de fuera hacia dentro.

ParteDescripción
ProfileSettingsLa sección: tarjeta de vista previa, foto, nombre, cómo te llama el asistente, nombre de usuario, biografía, perfil público, enlaces y correo.
ProfileCardLa vista previa en vivo del perfil público, con Copy link. Se puede usar por sí sola.
AvatarCropDialogRecorta una imagen a un cuadrado con arrastre, zoom y teclado, y devuelve una URL de datos.
ChangeEmailDialogEl cambio de correo en dos pasos: nueva dirección y luego un código de 6 dígitos.
useUsernameCheckComprobaciones de disponibilidad de un nombre de usuario con debounce, cancelables y en caché.

ProfileSettings

Úsalo dentro de un SettingsSection.

PropTipoPredeterminado
profileLo guardado ahora: { avatar, name, nickname, username, bio, links, public }. Cuando cambia y no hay ediciones, el borrador lo sigue.
ProfileValues–
onSaveSe llama con los valores recortados y los enlaces normalizados. Devuelve { field: message } para errores de campo, o lanza un error para mostrar el mensaje en la barra de guardado.
(values) => void | errors | Promise<void | errors>–
emailLa dirección de inicio de sesión.
string–
emailVerifiedMuestra Verified, o Not verified con Resend link.
booleantrue
checkUsernameComprueba si un nombre de usuario está libre. Recibe el nombre en minúsculas y un AbortSignal. Sin esto, solo se comprueba el formato.
(username, signal) => Promise<{ available, message?, suggestions? }>–
onRequestEmailCodeEnvía un código a una dirección nueva. Devuelve un mensaje para rechazar la dirección. Con onVerifyEmailCode, activa Change.
(email) => Promise<void | string>–
onVerifyEmailCodeComprueba el código. Devuelve false para un código incorrecto, o lanza un error para mostrar tu mensaje.
(email, code) => Promise<boolean>–
onEmailChangeSe llama una vez confirmada la nueva dirección.
(email) => void–
onResendVerificationAñade Resend link mientras la dirección no está verificada.
() => Promise<unknown>–
profileUrlLa URL del perfil público, mostrada bajo el nombre de usuario y copiada por Copy link.
(username) => stringhttps://hexta.app/@username
assistantNameSe usa en «What should Hexta call you?».
string"Hexta"
maxAvatarBytesFoto más grande admitida.
number5 MB
maxLinksCuántos enlaces puede tener un perfil.
number5
maxBioLongitud de la biografía, con un contador.
number160
PropTipoPredeterminado
profileQué mostrar.
{ avatar, name, username, bio, links, public }–
urlLa URL del perfil que mostrar y copiar.
string–
onCopiedSe llama después de copiar el enlace.
() => void–
PropTipoPredeterminado
sourceLa imagen que recortar. Abierto mientras esté definida.
{ url, width, height } | null–
onOpenChangeBorra la fuente al cerrarse.
(open) => void–
onApplyObtiene el cuadrado recortado como WebP, o PNG donde WebP no es compatible.
(dataUrl) => void–
outputSizeLado más largo de la salida en píxeles. Nunca amplía más allá del recorte.
number512
maxZoomHasta dónde puedes acercar.
number4
PropTipoPredeterminado
openSi está abierto.
boolean–
currentEmailSe muestra en la introducción y se rechaza como nueva dirección.
string–
onRequestCodeEnvía el código. Devuelve un mensaje para rechazar.
(email) => Promise<void | string>–
onVerifyCodeComprueba el código.
(email, code) => Promise<boolean>–
onChangedSe llama después de confirmar el código, antes de que se cierre el diálogo.
(email) => void–
resendAfterSegundos antes de que se desbloquee Resend code.
number30

useUsernameCheck(username, options)

Devuelve { status, result, problem, retry, resolve }. status es idle, invalid, checking, available, taken o error.

PropTipoPredeterminado
currentEl nombre de usuario guardado, que nunca se comprueba.
string–
checkTu comprobación de disponibilidad.
(username, signal) => Promise<UsernameCheck>–
delayMilisegundos de espera tras dejar de escribir.
number350
KeyAcción
EnterEn el último enlace, añade otro enlace y pasa a él.
←→↑↓En el diálogo de recorte, mueve la foto. Shift mueve más lejos.
+−En el diálogo de recorte, acerca y aleja. 0 restablece.
⌘SGuarda mientras haya cambios sin guardar. Ctrl+S en Windows y Linux.
EscCierra un diálogo y devuelve el foco al botón que lo abrió.
  • Cada campo se nombra con su fila, y los botones que repiten una palabra dicen sobre qué actúan: Upload photo, Change email, Remove link 2, github.com/you.
  • Los resultados del nombre de usuario, los enlaces copiados, los cambios de foto, los enlaces eliminados y un correo modificado se anuncian de forma polite. La comprobación en sí no se anuncia, para que escribir siga en silencio.
  • Los errores de campo marcan solo el campo con el fallo, y el foco pasa al primero cuando se rechaza un guardado. El código del correo anuncia comprobando, confirmado e incorrecto.
  • Al quitar un enlace, el foco pasa al siguiente enlace, o a Add link cuando no queda ninguno. Al quitar la foto, el foco pasa a Upload.
  • El área de recorte es enfocable y funciona con las teclas de flecha y más y menos, y el control deslizante de zoom indica su valor como porcentaje.
  • En pantallas táctiles, los inputs usan texto de 16px para que iOS no haga zoom, y los botones de icono tienen objetivos de 44px.

Construido con

Los componentes gratuitos de HextaUI con los que está hecho Profile. Cada uno se instala por separado.

Código

6 archivos, añadidos a components/blocks/profile.