Profile Form
Install this component from the Scintillar registry.
Controls
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>
)
}