プロフィール

AIプロダクトの設定にあるプロフィールセクションです。写真を円形に切り抜き、入力しながら確認されるユーザー名を選び、6桁のコードで新しいメールアドレスを確認し、サイトを認識するリンクを追加し、他の人からどう見えるかをライブのカードで確認できます。

ProfileSettingsは、ClaudeやCursorのようなプロダクトのプロフィールページで、どのSettingsShellのセクションにも組み込めるよう作られています。保存済みのプロフィールとonSaveを渡すだけで、残りを処理します。指示したときだけ保存される下書き、チェックやサーバーからのフィールドエラー、Settingsの⌘Sに対応した浮かぶ保存バーです。

上部のカードには、他の人からあなたのプロフィールがどう見えるかが表示され、入力に合わせて変わります。写真、名前、ハンドル、自己紹介、リンクです。Show my profileをオフにすると、カードが暗くなり、自分だけに表示されることが示されます。Copy linkはプロフィールのURLをクリップボードにコピーします。

写真を選ぶか、行にドロップすると、切り抜きダイアログで開きます。ドラッグで移動し、スライダー、ホイール、トラックパッドのピンチでズームし、写真はブラウザー内で正方形に切り抜かれてから、データURLとしてonSaveに渡されます。種類が違う、大きすぎる、128ピクセル未満のファイルは、理由とともに行内で拒否されます。

ユーザー名は、入力を遅くすることなく、入力しながら確認されます。確認は最後のキー入力の350ms後に行われ、古くなった確認はAbortSignalでキャンセルされ、すでに確認した名前には即座に応答します。使用済みの名前には、ワンタップで直せる候補が表示されます。確認が終わる前に保存すると、保存は確認の完了を待ちます。

メールアドレスは下書きに含まれません。Changeは2ステップのダイアログを開きます。新しいアドレスを入力し、そこに送られた6桁のコードを入力すると、最後の桁を入れた時点で自動的に確認されます。間違ったコードは揺れてクリアされ、Resendは30秒後に使えるようになり、コードが確認されると行が更新されます。未確認のアドレスにはResend linkが表示されます。

リンクには、貼り付けられたものがそのまま使えます。github.com/you、完全なURL、wwwで始まるアドレスです。既知のサイトにはそのアイコンが、それ以外には先頭の文字が付きます。保存時にはhttpsのURLに正規化され、空のものは削除されます。リンクではないアドレスや、他と重複するアドレスは、1つずつマークされます。

  1. Proレジストリをcomponents.jsonに追加する

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. トークンを追加する

    アカウントページでトークンを作成し、.env.local に HEXTAUI_PRO_TOKEN として設定してください。

  3. ブロックを追加する

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

APIに接続する

保存済みのプロフィールと、フィールドエラーを返すかエラーを投げるonSaveを渡します。checkUsernameはAbortSignalを受け取るため、入力中に古くなった確認はキャンセルされます。

"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>
  )
}

メールアドレスを変更して確認する

Changeボタンを有効にするには、onRequestEmailCodeとonVerifyEmailCodeを追加します。アドレスを拒否するにはonRequestEmailCodeからメッセージを返し、コードが間違っている場合はonVerifyEmailCodeからfalseを返します。

"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>
  )
}

構造

外側から内側へ組み合わせるパーツ。

パーツ説明
ProfileSettingsセクション。プレビューカード、写真、名前、アシスタントからの呼ばれ方、ユーザー名、自己紹介、公開プロフィール、リンク、メール。
ProfileCard公開プロフィールのライブプレビューとCopy link。単独でも使えます。
AvatarCropDialogドラッグ、ズーム、キーボードで画像を正方形に切り抜き、データURLを返します。
ChangeEmailDialog2ステップのメールアドレス変更。新しいアドレス、続いて6桁のコード。
useUsernameCheckユーザー名に対する、デバウンスされ、キャンセル可能で、キャッシュされた利用可否の確認。

ProfileSettings

SettingsSectionの内側で使います。

プロパティ型デフォルト
profile現在保存されている内容: { avatar, name, nickname, username, bio, links, public }。変更があり、編集がない場合、下書きはそれに追従します。
ProfileValues–
onSave前後の空白が削除された値と、正規化されたリンクとともに呼ばれます。フィールドエラーには { field: message } を返し、保存バーにメッセージを表示するにはエラーを投げます。
(values) => void | errors | Promise<void | errors>–
emailサインインに使うアドレス。
string–
emailVerifiedVerifiedを、または未確認の場合はResend link付きのNot verifiedを表示します。
booleantrue
checkUsernameユーザー名が使えるかを確認します。小文字化された名前とAbortSignalを受け取ります。指定しない場合は、形式のみが確認されます。
(username, signal) => Promise<{ available, message?, suggestions? }>–
onRequestEmailCode新しいアドレスにコードを送信します。アドレスを拒否するにはメッセージを返します。onVerifyEmailCodeとともに指定すると、Changeが有効になります。
(email) => Promise<void | string>–
onVerifyEmailCodeコードを確認します。コードが間違っている場合はfalseを返し、独自のメッセージを表示するにはエラーを投げます。
(email, code) => Promise<boolean>–
onEmailChange新しいアドレスが確認されたときに呼ばれます。
(email) => void–
onResendVerificationアドレスが未確認の間、Resend linkを追加します。
() => Promise<unknown>–
profileUrl公開プロフィールのURL。ユーザー名の下に表示され、Copy linkでコピーされます。
(username) => stringhttps://hexta.app/@username
assistantName「What should Hexta call you?」で使われます。
string"Hexta"
maxAvatarBytes受け付ける写真の最大サイズ。
number5 MB
maxLinksプロフィールに設定できるリンクの数。
number5
maxBio自己紹介の長さ。カウンター付きです。
number160
プロパティ型デフォルト
profile表示する内容。
{ avatar, name, username, bio, links, public }–
url表示してコピーするプロフィールのURL。
string–
onCopiedリンクがコピーされた後に呼ばれます。
() => void–
プロパティ型デフォルト
source切り抜く画像。設定されている間、開きます。
{ url, width, height } | null–
onOpenChange閉じるときに、ソースをクリアします。
(open) => void–
onApply切り抜いた正方形をWebPで取得します。WebPが非対応の場合はPNGです。
(dataUrl) => void–
outputSize出力の最長辺(ピクセル)。切り抜きより大きく拡大されることはありません。
number512
maxZoomどこまでズームインできるか。
number4
プロパティ型デフォルト
open開いているかどうか。
boolean–
currentEmail紹介文に表示され、新しいアドレスとしては拒否されます。
string–
onRequestCodeコードを送信します。拒否するにはメッセージを返します。
(email) => Promise<void | string>–
onVerifyCodeコードを確認します。
(email, code) => Promise<boolean>–
onChangedコードが確認された後、ダイアログが閉じる前に呼ばれます。
(email) => void–
resendAfterResend codeが使えるようになるまでの秒数。
number30

useUsernameCheck(username, options)

{ status, result, problem, retry, resolve } を返します。statusは、idle、invalid、checking、available、taken、errorのいずれかです。

プロパティ型デフォルト
current保存済みのユーザー名。確認の対象にはなりません。
string–
check利用可否の確認。
(username, signal) => Promise<UsernameCheck>–
delay入力が止まってから待機するミリ秒数。
number350
キーアクション
Enter最後のリンクで、別のリンクを追加してそこへ移動します。
←→↑↓切り抜きダイアログで、写真を移動します。Shiftを押すとより大きく動きます。
+−切り抜きダイアログで、ズームインとズームアウトをします。0でリセットします。
⌘S未保存の変更があるときに保存します。WindowsとLinuxではCtrl+Sです。
Escダイアログを閉じ、開いたボタンにフォーカスを戻します。
  • すべてのフィールドには行の名前が付き、同じ単語を繰り返すボタンには、操作の対象が示されます。Upload photo、Change email、Remove link 2、github.com/you などです。
  • ユーザー名の結果、コピーされたリンク、写真の変更、削除されたリンク、変更されたメールアドレスは、politeに読み上げられます。確認中自体は読み上げられないため、入力は静かなままです。
  • フィールドエラーは問題のあるフィールドだけにマークされ、保存が拒否されると最初のフィールドにフォーカスが移ります。メールのコードは、確認中、確認済み、不正を読み上げます。
  • リンクを削除すると、フォーカスが次のリンクに、残っていなければAdd linkに移ります。写真を削除すると、フォーカスがUploadに移ります。
  • 切り抜き領域はフォーカス可能で、矢印キーとプラス・マイナスキーで操作でき、ズームスライダーは値をパーセンテージで読み上げます。
  • タッチ画面では、入力欄のテキストが16pxになりiOSでズームされず、アイコンボタンは44pxのターゲットになります。

使用技術

Profile を構成する無料のHextaUIコンポーネントです。それぞれ単独でインストールできます。

コード

6 個のファイルを components/blocks/profile に追加しました。