Skip to content

Profile Form

Install this component from the Scintillar registry.

Profile
Manage your public profile information.
Click to upload a new avatar

Controls

Installation

npx shadcn@latest add @scintillar/profile-form

Source

"use client"

import { useState } from "react"
import { Camera } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { FormSection } from "@/registry/new-york/blocks/form-section/form-section"

export interface ProfileFormProps {
  /** User display name. */
  name?: string
  /** User email address (displayed as readonly). */
  email?: string
  /** User bio / about text. */
  bio?: string
  /** Called when the user saves the form. */
  onSave?: (data: { name: string; bio: string }) => void
  /** Called when the user cancels editing. */
  onCancel?: () => void
  /** Whether to show the avatar upload area. */
  showAvatar?: boolean
  /** Current avatar image URL. */
  avatarUrl?: string
  /** Called when the user selects a new avatar file. */
  onAvatarChange?: (file: File) => void
  /** Additional CSS classes. */
  className?: string
}

/**
 * A profile form section for editing user display name, bio, and optional avatar.
 * Email is shown as readonly. Uses FormSection for consistent layout.
 */
export function ProfileForm({
  name: initialName = "",
  email = "",
  bio: initialBio = "",
  onSave,
  onCancel,
  showAvatar = false,
  avatarUrl,
  onAvatarChange,
  className,
}: ProfileFormProps) {
  const [name, setName] = useState(initialName)
  const [bio, setBio] = useState(initialBio)

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    onSave?.({ name, bio })
  }

  function handleAvatarSelect(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0]
    if (file) onAvatarChange?.(file)
  }

  return (
    <form onSubmit={handleSubmit} className={className}>
      <FormSection
        title="Profile"
        description="Manage your public profile information."
        footer={
          <>
            <Button type="button" variant="outline" onClick={onCancel}>
              Cancel
            </Button>
            <Button type="submit">Save</Button>
          </>
        }
      >
        <div className="space-y-6">
          {showAvatar && (
            <div className="flex items-center gap-4">
              <div className="relative group">
                <div
                  className={cn(
                    "flex size-20 items-center justify-center overflow-hidden rounded-full bg-muted",
                    !avatarUrl && "border-2 border-dashed border-muted-foreground/25"
                  )}
                >
                  {avatarUrl ? (
                    // eslint-disable-next-line @next/next/no-img-element -- avatar from user-provided URL, not a static asset
                    <img
                      src={avatarUrl}
                      alt="Avatar"
                      className="size-full object-cover"
                    />
                  ) : (
                    <Camera className="size-6 text-muted-foreground" />
                  )}
                </div>
                <label
                  htmlFor="avatar-upload"
                  className="absolute inset-0 flex cursor-pointer items-center justify-center rounded-full bg-black/50 text-white opacity-0 transition-opacity group-hover:opacity-100"
                >
                  <Camera className="size-5" />
                </label>
                <input
                  id="avatar-upload"
                  type="file"
                  accept="image/*"
                  className="sr-only"
                  onChange={handleAvatarSelect}
                />
              </div>
              <div className="text-sm text-muted-foreground">
                Click to upload a new avatar
              </div>
            </div>
          )}

          <div className="space-y-2">
            <Label htmlFor="profile-name">Name</Label>
            <Input
              id="profile-name"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="Your name"
            />
          </div>

          <div className="space-y-2">
            <Label htmlFor="profile-email">Email</Label>
            <Input
              id="profile-email"
              type="email"
              value={email}
              readOnly
              className="bg-muted"
            />
          </div>

          <div className="space-y-2">
            <Label htmlFor="profile-bio">Bio</Label>
            <Textarea
              id="profile-bio"
              value={bio}
              onChange={(e) => setBio(e.target.value)}
              placeholder="Tell us about yourself"
              rows={4}
            />
          </div>
        </div>
      </FormSection>
    </form>
  )
}