bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
17 lines • 9.91 kB
JSON
{
"name": "profile-management-profileeditor",
"type": "registry:component",
"dependencies": [
"@radix-ui/react-icons"
],
"devDependencies": [],
"registryDependencies": [],
"files": [
{
"path": "components/profile-management/ProfileEditor.tsx",
"type": "registry:component",
"content": "\"use client\";\n\nimport { CheckIcon, ImageIcon, PersonIcon } from \"@radix-ui/react-icons\";\nimport { useState } from \"react\";\nimport { CONTAINER_WIDTHS } from \"../../lib/layout-constants.js\";\nimport type { ProfileInfo } from \"../../lib/types.js\";\nimport { cn } from \"../../lib/utils.js\";\nimport { LoadingButton } from \"../ui-components/LoadingButton.js\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Card, CardContent } from \"../ui/card.js\";\nimport {\n\tDialog,\n\tDialogClose,\n\tDialogContent,\n\tDialogDescription,\n\tDialogTitle,\n\tDialogTrigger,\n} from \"../ui/dialog.js\";\nimport { Input } from \"../ui/input.js\";\nimport { Textarea } from \"../ui/textarea.js\";\n\nexport interface ProfileEditorProps {\n\tprofile: ProfileInfo;\n\tonSave: (updates: Partial<ProfileInfo>) => Promise<void>;\n\tonCancel?: () => void;\n\tallowImageUpload?: boolean;\n\tmaxNameLength?: number;\n\tmaxDescriptionLength?: number;\n\tclassName?: string;\n\tvariant?: \"default\" | \"outline\" | \"ghost\";\n\tsize?: \"sm\" | \"default\" | \"lg\";\n}\n\n/**\n * ProfileEditor allows editing BAP profile information\n *\n * Features:\n * - Edit profile name, image URL, and description\n * - Avatar preview with fallback\n * - Character limits and validation\n * - Save/cancel actions\n * - Loading states\n * - Error handling\n */\nexport function ProfileEditor({\n\tprofile,\n\tonSave,\n\tonCancel,\n\tallowImageUpload: _allowImageUpload = false,\n\tmaxNameLength = 50,\n\tmaxDescriptionLength = 200,\n\tclassName = \"\",\n\tvariant = \"default\",\n\tsize = \"default\",\n}: ProfileEditorProps) {\n\t// Helper to convert size prop to valid input size\n\tconst inputSize = size === \"lg\" ? \"default\" : size;\n\tconst [name, setName] = useState(profile.name || \"\");\n\tconst [image, setImage] = useState(profile.image || \"\");\n\tconst [description, setDescription] = useState(profile.description || \"\");\n\tconst [isLoading, setIsLoading] = useState(false);\n\tconst [error, setError] = useState<string | null>(null);\n\tconst [showImageDialog, setShowImageDialog] = useState(false);\n\n\tconst hasChanges =\n\t\tname !== (profile.name || \"\") ||\n\t\timage !== (profile.image || \"\") ||\n\t\tdescription !== (profile.description || \"\");\n\n\tconst isValid =\n\t\tname.length > 0 &&\n\t\tname.length <= maxNameLength &&\n\t\tdescription.length <= maxDescriptionLength;\n\n\tconst handleSave = async () => {\n\t\tif (!isValid || !hasChanges) return;\n\n\t\tsetIsLoading(true);\n\t\tsetError(null);\n\n\t\ttry {\n\t\t\tawait onSave({\n\t\t\t\tname: name.trim(),\n\t\t\t\timage: image.trim() || undefined,\n\t\t\t\tdescription: description.trim() || undefined,\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tsetError(err instanceof Error ? err.message : \"Failed to save profile\");\n\t\t\tsetIsLoading(false);\n\t\t}\n\t};\n\n\tconst handleImageChange = (url: string) => {\n\t\tsetImage(url);\n\t\tsetShowImageDialog(false);\n\t};\n\n\tconst getInitials = (profileName?: string) => {\n\t\tif (!profileName) return \"?\";\n\t\treturn profileName\n\t\t\t.split(\" \")\n\t\t\t.map((word) => word[0])\n\t\t\t.join(\"\")\n\t\t\t.toUpperCase()\n\t\t\t.slice(0, 2);\n\t};\n\n\treturn (\n\t\t<>\n\t\t\t<Card\n\t\t\t\tclassName={cn(\n\t\t\t\t\tvariant === \"outline\" && \"border\",\n\t\t\t\t\tvariant === \"ghost\" && \"border-0 shadow-none\",\n\t\t\t\t\tclassName,\n\t\t\t\t)}\n\t\t\t\tstyle={{ maxWidth: CONTAINER_WIDTHS.FORM_DEFAULT }}\n\t\t\t>\n\t\t\t\t<CardContent className=\"p-6\">\n\t\t\t\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t\t\t\t<h3 className=\"text-lg font-bold\">Edit Profile</h3>\n\n\t\t\t\t\t\t{/* Avatar Preview */}\n\t\t\t\t\t\t<div className=\"flex items-center gap-4\">\n\t\t\t\t\t\t\t<Avatar className=\"h-16 w-16\">\n\t\t\t\t\t\t\t\t<AvatarImage src={image} />\n\t\t\t\t\t\t\t\t<AvatarFallback>\n\t\t\t\t\t\t\t\t\t{getInitials(name || profile.name)}\n\t\t\t\t\t\t\t\t</AvatarFallback>\n\t\t\t\t\t\t\t</Avatar>\n\t\t\t\t\t\t\t<div className=\"flex flex-col gap-1 flex-1\">\n\t\t\t\t\t\t\t\t<p className=\"text-sm font-medium\">Profile Picture</p>\n\t\t\t\t\t\t\t\t<div className=\"flex gap-2\">\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\tonClick={() => setShowImageDialog(true)}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<ImageIcon className=\"w-4 h-4 mr-1\" />\n\t\t\t\t\t\t\t\t\t\tChange Image\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t{image && (\n\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"text-destructive\"\n\t\t\t\t\t\t\t\t\t\t\tonClick={() => setImage(\"\")}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\tRemove\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t{/* Name Field */}\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div className=\"flex items-center justify-between mb-1\">\n\t\t\t\t\t\t\t\t<p className=\"text-sm font-medium\">Display Name</p>\n\t\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t{name.length}/{maxNameLength}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"relative\">\n\t\t\t\t\t\t\t\t<PersonIcon className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4\" />\n\t\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\t\tvalue={name}\n\t\t\t\t\t\t\t\t\tonChange={(e) => setName(e.target.value)}\n\t\t\t\t\t\t\t\t\tplaceholder=\"Enter your name\"\n\t\t\t\t\t\t\t\t\tmaxLength={maxNameLength}\n\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\"pl-10\",\n\t\t\t\t\t\t\t\t\t\tinputSize === \"sm\" && \"h-8\",\n\t\t\t\t\t\t\t\t\t\tinputSize === \"default\" && \"h-10\",\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t{/* Description Field */}\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div className=\"flex items-center justify-between mb-1\">\n\t\t\t\t\t\t\t\t<p className=\"text-sm font-medium\">Bio</p>\n\t\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t{description.length}/{maxDescriptionLength}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<Textarea\n\t\t\t\t\t\t\t\tvalue={description}\n\t\t\t\t\t\t\t\tonChange={(e) => setDescription(e.target.value)}\n\t\t\t\t\t\t\t\tplaceholder=\"Tell us about yourself\"\n\t\t\t\t\t\t\t\tmaxLength={maxDescriptionLength}\n\t\t\t\t\t\t\t\tclassName=\"min-h-[80px]\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t{/* Error Display */}\n\t\t\t\t\t\t{error && (\n\t\t\t\t\t\t\t<Card className=\"bg-red-50 border-red-200\">\n\t\t\t\t\t\t\t\t<CardContent className=\"p-3\">\n\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-red-600\">{error}</p>\n\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t{/* Actions */}\n\t\t\t\t\t\t<div className=\"flex gap-3 justify-end\">\n\t\t\t\t\t\t\t{onCancel && (\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t\t\tonClick={onCancel}\n\t\t\t\t\t\t\t\t\tdisabled={isLoading}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\tCancel\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t<LoadingButton\n\t\t\t\t\t\t\t\tloading={isLoading}\n\t\t\t\t\t\t\t\tdisabled={!hasChanges || !isValid}\n\t\t\t\t\t\t\t\tonClick={handleSave}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<CheckIcon className=\"w-4 h-4 mr-1\" />\n\t\t\t\t\t\t\t\tSave Changes\n\t\t\t\t\t\t\t</LoadingButton>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\n\t\t\t{/* Image URL Dialog */}\n\t\t\t<Dialog open={showImageDialog} onOpenChange={setShowImageDialog}>\n\t\t\t\t<DialogContent className=\"max-w-[450px]\">\n\t\t\t\t\t<DialogTitle>Set Profile Image</DialogTitle>\n\t\t\t\t\t<DialogDescription className=\"mb-4\">\n\t\t\t\t\t\tEnter the URL of your profile image. Make sure it's a valid image\n\t\t\t\t\t\tURL.\n\t\t\t\t\t</DialogDescription>\n\n\t\t\t\t\t<div className=\"flex flex-col gap-3\">\n\t\t\t\t\t\t<div className=\"relative\">\n\t\t\t\t\t\t\t<ImageIcon className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4\" />\n\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\tvalue={image}\n\t\t\t\t\t\t\t\tonChange={(e) => setImage(e.target.value)}\n\t\t\t\t\t\t\t\tplaceholder=\"https://example.com/image.jpg\"\n\t\t\t\t\t\t\t\tclassName=\"pl-10\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t{image && (\n\t\t\t\t\t\t\t<Card className=\"bg-muted\">\n\t\t\t\t\t\t\t\t<CardContent className=\"p-3\">\n\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-3\">\n\t\t\t\t\t\t\t\t\t\t<Avatar className=\"h-12 w-12\">\n\t\t\t\t\t\t\t\t\t\t\t<AvatarImage src={image} />\n\t\t\t\t\t\t\t\t\t\t\t<AvatarFallback>?</AvatarFallback>\n\t\t\t\t\t\t\t\t\t\t</Avatar>\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm\">Preview</p>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t<div className=\"flex gap-3 mt-4 justify-end\">\n\t\t\t\t\t\t\t<DialogClose asChild>\n\t\t\t\t\t\t\t\t<Button variant=\"secondary\">Cancel</Button>\n\t\t\t\t\t\t\t</DialogClose>\n\t\t\t\t\t\t\t<DialogClose asChild>\n\t\t\t\t\t\t\t\t<Button onClick={() => handleImageChange(image)}>Apply</Button>\n\t\t\t\t\t\t\t</DialogClose>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</DialogContent>\n\t\t\t</Dialog>\n\t\t</>\n\t);\n}\n",
"target": "<%- config.aliases.components %>/profileeditor.tsx"
}
]
}