Components/Profiles
ProfileEditor
Edit BAP profile information with validation, image preview, and save/cancel actions
A component for editing BAP (Bitcoin Attestation Protocol) profile information, including name, description, image URL, and other profile fields with validation and preview functionality.
Installation
npx bigblocks add profile-editor
Import
import { ProfileEditor } from 'bigblocks';
Props
Prop | Type | Required | Default | Description |
---|---|---|---|---|
profile | ProfileInfo | Yes | - | Current profile data to edit |
onSave | (updates: Partial<ProfileInfo>) => Promise<void> | Yes | - | Save handler for profile updates |
onCancel | () => void | No | - | Cancel button click handler |
allowImageUpload | boolean | No | true | Enable image URL input field |
maxNameLength | number | No | 50 | Maximum character limit for name |
maxDescriptionLength | number | No | 160 | Maximum character limit for description |
className | string | No | - | Additional CSS classes |
variant | 'surface' | 'classic' | 'ghost' | No | 'surface' | Visual style variant |
size | '1' | '2' | '3' | '4' | No | '3' | Component size |
Basic Usage
import { ProfileEditor } from 'bigblocks';
export default function EditProfile() {
const [profile, setProfile] = useState({
id: 'bap-123',
address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
isPublished: false,
name: 'Bitcoin Builder',
description: 'Building the future of money',
image: 'https://api.dicebear.com/7.x/avataaars/svg?seed=builder'
});
const handleSave = async (updates) => {
try {
// Merge updates with current profile
const updatedProfile = { ...profile, ...updates };
// Save to backend
await updateProfile(profile.id, updates);
// Update local state
setProfile(updatedProfile);
console.log('Profile saved successfully');
} catch (error) {
console.error('Failed to save profile:', error);
}
};
const handleCancel = () => {
console.log('Edit cancelled');
// Navigate back or close modal
};
return (
<ProfileEditor
profile={profile}
onSave={handleSave}
onCancel={handleCancel}
/>
);
}
Advanced Usage
Complete Profile Editing
import { ProfileEditor } from 'bigblocks';
export default function CompleteProfileEditor() {
const [profile, setProfile] = useState({
id: 'bap-456',
address: '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2',
isPublished: true,
'@context': 'https://schema.org',
'@type': 'Person',
name: 'Satoshi Nakamoto',
alternateName: '@satoshi',
description: 'Creator of Bitcoin and digital currency pioneer.',
image: 'https://api.dicebear.com/7.x/avataaars/svg?seed=satoshi',
banner: 'https://images.unsplash.com/photo-1640340434855-6084b1f4901c',
url: 'https://bitcoin.org',
email: 'satoshi@gmx.com',
paymail: 'satoshi@relayx.io',
givenName: 'Satoshi',
familyName: 'Nakamoto',
location: 'Worldwide'
});
const handleSave = async (updates) => {
try {
// Validate updates
if (updates.name && updates.name.length < 3) {
throw new Error('Name must be at least 3 characters');
}
// Save to blockchain if profile is published
if (profile.isPublished) {
await publishProfileUpdate(profile.id, updates);
} else {
await saveProfileDraft(profile.id, updates);
}
setProfile(prev => ({ ...prev, ...updates }));
toast.success('Profile updated successfully!');
} catch (error) {
toast.error(`Failed to save: ${error.message}`);
}
};
return (
<ProfileEditor
profile={profile}
onSave={handleSave}
onCancel={() => router.back()}
maxNameLength={50}
maxDescriptionLength={280}
variant="surface"
size="4"
/>
);
}
With Loading State
import { ProfileEditor } from 'bigblocks';
import { useState } from 'react';
export default function ProfileEditorWithLoading() {
const [profile, setProfile] = useState(null);
const [saving, setSaving] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadProfile = async () => {
try {
const profileData = await fetchProfile();
setProfile(profileData);
} catch (error) {
console.error('Failed to load profile:', error);
} finally {
setLoading(false);
}
};
loadProfile();
}, []);
const handleSave = async (updates) => {
setSaving(true);
try {
await updateProfile(profile.id, updates);
setProfile(prev => ({ ...prev, ...updates }));
// Show success message
toast.success('Profile saved!');
// Navigate away
router.push('/profile');
} catch (error) {
toast.error('Failed to save profile');
} finally {
setSaving(false);
}
};
if (loading) {
return <div>Loading profile...</div>;
}
if (!profile) {
return <div>Profile not found</div>;
}
return (
<div>
{saving && (
<div className="mb-4 p-3 bg-blue-100 text-blue-700 rounded">
Saving profile...
</div>
)}
<ProfileEditor
profile={profile}
onSave={handleSave}
onCancel={() => router.back()}
/>
</div>
);
}
In Modal Dialog
import { ProfileEditor, Modal } from 'bigblocks';
export default function ProfileEditModal({ isOpen, onClose, profile }) {
const [hasChanges, setHasChanges] = useState(false);
const handleSave = async (updates) => {
try {
await updateProfile(profile.id, updates);
setHasChanges(false);
onClose();
toast.success('Profile updated!');
} catch (error) {
toast.error('Failed to save profile');
}
};
const handleCancel = () => {
if (hasChanges) {
const confirmed = confirm('You have unsaved changes. Are you sure you want to cancel?');
if (!confirmed) return;
}
setHasChanges(false);
onClose();
};
return (
<Modal isOpen={isOpen} onClose={handleCancel}>
<div className="p-6">
<h2 className="text-xl font-semibold mb-4">Edit Profile</h2>
<ProfileEditor
profile={profile}
onSave={handleSave}
onCancel={handleCancel}
onChange={() => setHasChanges(true)}
/>
</div>
</Modal>
);
}
Different Variants and Sizes
import { ProfileEditor } from 'bigblocks';
export default function ProfileEditorVariants({ profile }) {
const handleSave = async (updates) => {
await updateProfile(profile.id, updates);
};
return (
<div className="space-y-8">
{/* Surface variant (default) */}
<div>
<h3>Surface Variant</h3>
<ProfileEditor
profile={profile}
onSave={handleSave}
variant="surface"
size="3"
/>
</div>
{/* Classic variant */}
<div>
<h3>Classic Variant</h3>
<ProfileEditor
profile={profile}
onSave={handleSave}
variant="classic"
size="3"
/>
</div>
{/* Ghost variant */}
<div>
<h3>Ghost Variant</h3>
<ProfileEditor
profile={profile}
onSave={handleSave}
variant="ghost"
size="3"
/>
</div>
</div>
);
}
Custom Validation
import { ProfileEditor } from 'bigblocks';
export default function ValidatedProfileEditor({ profile }) {
const [errors, setErrors] = useState({});
const validateProfile = (updates) => {
const newErrors = {};
if (updates.name !== undefined) {
if (!updates.name || updates.name.trim().length < 3) {
newErrors.name = 'Name must be at least 3 characters';
}
if (updates.name && updates.name.length > 50) {
newErrors.name = 'Name must be less than 50 characters';
}
}
if (updates.description !== undefined && updates.description) {
if (updates.description.length > 280) {
newErrors.description = 'Description must be less than 280 characters';
}
}
if (updates.email !== undefined && updates.email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(updates.email)) {
newErrors.email = 'Please enter a valid email address';
}
}
if (updates.url !== undefined && updates.url) {
try {
new URL(updates.url);
} catch {
newErrors.url = 'Please enter a valid URL';
}
}
return newErrors;
};
const handleSave = async (updates) => {
const validationErrors = validateProfile(updates);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
setErrors({});
try {
await updateProfile(profile.id, updates);
toast.success('Profile saved successfully!');
} catch (error) {
toast.error('Failed to save profile');
}
};
return (
<div>
{Object.keys(errors).length > 0 && (
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
<ul>
{Object.values(errors).map((error, index) => (
<li key={index}>• {error}</li>
))}
</ul>
</div>
)}
<ProfileEditor
profile={profile}
onSave={handleSave}
maxNameLength={50}
maxDescriptionLength={280}
/>
</div>
);
}
Auto-save Implementation
import { ProfileEditor } from 'bigblocks';
import { useDebounce } from 'use-debounce';
export default function AutoSaveProfileEditor({ profile }) {
const [pendingUpdates, setPendingUpdates] = useState({});
const [debouncedUpdates] = useDebounce(pendingUpdates, 2000);
const [lastSaved, setLastSaved] = useState(null);
// Auto-save when debounced updates change
useEffect(() => {
if (Object.keys(debouncedUpdates).length > 0) {
autoSave(debouncedUpdates);
}
}, [debouncedUpdates]);
const autoSave = async (updates) => {
try {
await updateProfile(profile.id, updates);
setLastSaved(new Date());
setPendingUpdates({});
// Silent save - no toast notification
console.log('Profile auto-saved');
} catch (error) {
console.error('Auto-save failed:', error);
}
};
const handleManualSave = async (updates) => {
try {
await updateProfile(profile.id, { ...pendingUpdates, ...updates });
setPendingUpdates({});
setLastSaved(new Date());
toast.success('Profile saved!');
} catch (error) {
toast.error('Failed to save profile');
}
};
const handleChange = (updates) => {
setPendingUpdates(prev => ({ ...prev, ...updates }));
};
return (
<div>
<div className="mb-4 flex justify-between items-center">
<div className="text-sm text-gray-500">
{lastSaved ? (
`Last saved: ${lastSaved.toLocaleTimeString()}`
) : (
'No changes saved yet'
)}
</div>
{Object.keys(pendingUpdates).length > 0 && (
<div className="text-sm text-orange-600">
Unsaved changes...
</div>
)}
</div>
<ProfileEditor
profile={profile}
onSave={handleManualSave}
onChange={handleChange}
/>
</div>
);
}
Common Patterns
Profile Setup Flow
import { ProfileEditor } from 'bigblocks';
export default function ProfileSetupFlow() {
const [step, setStep] = useState(1);
const [profile, setProfile] = useState({
id: 'new-profile',
address: '',
isPublished: false,
name: '',
description: '',
image: ''
});
const handleSave = async (updates) => {
const updatedProfile = { ...profile, ...updates };
// Validate required fields for next step
if (step === 1 && (!updatedProfile.name || updatedProfile.name.length < 3)) {
toast.error('Please enter a valid name to continue');
return;
}
setProfile(updatedProfile);
if (step < 3) {
setStep(step + 1);
} else {
// Final save
await createProfile(updatedProfile);
router.push('/profile');
}
};
const getStepTitle = () => {
switch (step) {
case 1: return 'Basic Information';
case 2: return 'Profile Image';
case 3: return 'Description';
default: return 'Profile Setup';
}
};
return (
<div className="max-w-2xl mx-auto">
<div className="mb-6">
<h1 className="text-2xl font-bold">Set Up Your Profile</h1>
<p className="text-gray-600">Step {step} of 3: {getStepTitle()}</p>
</div>
<ProfileEditor
profile={profile}
onSave={handleSave}
onCancel={() => step > 1 ? setStep(step - 1) : router.back()}
maxNameLength={50}
maxDescriptionLength={160}
/>
</div>
);
}
Organization Profile Editor
import { ProfileEditor } from 'bigblocks';
export default function OrganizationEditor({ orgProfile }) {
const handleSave = async (updates) => {
// Add organization-specific validation
if (updates['@type'] && updates['@type'] !== 'Organization') {
toast.error('This editor is for organizations only');
return;
}
try {
await updateOrganizationProfile(orgProfile.id, updates);
toast.success('Organization profile updated!');
} catch (error) {
toast.error('Failed to update organization');
}
};
return (
<div>
<h2 className="text-xl font-semibold mb-4">Edit Organization</h2>
<ProfileEditor
profile={orgProfile}
onSave={handleSave}
maxNameLength={100} // Longer names for organizations
maxDescriptionLength={500} // Longer descriptions
allowImageUpload={true}
/>
</div>
);
}
Bulk Profile Updates
import { ProfileEditor } from 'bigblocks';
export default function BulkProfileEditor({ profiles }) {
const [selectedProfile, setSelectedProfile] = useState(profiles[0]);
const [allUpdates, setAllUpdates] = useState({});
const handleSave = async (updates) => {
// Save updates for current profile
setAllUpdates(prev => ({
...prev,
[selectedProfile.id]: { ...prev[selectedProfile.id], ...updates }
}));
toast.success('Changes saved for ' + selectedProfile.name);
};
const applyAllUpdates = async () => {
try {
const promises = Object.entries(allUpdates).map(([profileId, updates]) =>
updateProfile(profileId, updates)
);
await Promise.all(promises);
toast.success(`Updated ${Object.keys(allUpdates).length} profiles!`);
setAllUpdates({});
} catch (error) {
toast.error('Failed to apply some updates');
}
};
return (
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Profile selector */}
<div className="lg:col-span-1">
<h3 className="font-semibold mb-3">Select Profile</h3>
<div className="space-y-2">
{profiles.map(profile => (
<button
key={profile.id}
onClick={() => setSelectedProfile(profile)}
className={`w-full text-left p-3 rounded ${
selectedProfile.id === profile.id
? 'bg-blue-100 text-blue-700'
: 'bg-gray-50 hover:bg-gray-100'
}`}
>
{profile.name}
{allUpdates[profile.id] && (
<span className="text-orange-600 text-sm ml-2">•</span>
)}
</button>
))}
</div>
{Object.keys(allUpdates).length > 0 && (
<button
onClick={applyAllUpdates}
className="w-full mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
>
Apply All Changes ({Object.keys(allUpdates).length})
</button>
)}
</div>
{/* Profile editor */}
<div className="lg:col-span-3">
<ProfileEditor
key={selectedProfile.id} // Force re-render when profile changes
profile={selectedProfile}
onSave={handleSave}
/>
</div>
</div>
);
}
Form Fields
The ProfileEditor includes these editable fields:
Basic Fields
- Name: User's display name (required)
- Description: Bio or profile description
- Image URL: Avatar image URL
Extended Fields (if supported by profile schema)
- Alternate Name: Username or handle
- Email: Contact email address
- Website URL: Personal or company website
- Paymail: Bitcoin paymail address
- Location: Geographic location
Validation Rules
Name Field
- Required: Must not be empty
- Minimum Length: 3 characters (configurable)
- Maximum Length: 50 characters (configurable)
- Character Set: Alphanumeric and common punctuation
Description Field
- Optional: Can be empty
- Maximum Length: 160 characters (configurable)
- Line Breaks: Preserved in saved data
Image URL Field
- Optional: Can be empty
- Format: Must be valid URL if provided
- Preview: Shows image preview when valid URL entered
Email Field
- Optional: Can be empty
- Format: Must be valid email format if provided
- Validation: Real-time email format checking
Character Counters
- Live character count display
- Warning when approaching limit
- Error state when limit exceeded
- Visual indicator (color changes)
Image Preview
- Real-time preview of avatar image
- Fallback to default avatar if URL invalid
- Loading state while image loads
- Error handling for broken images
Features
- Real-time Validation: Immediate feedback on field requirements
- Character Limits: Configurable limits with live counters
- Image Preview: Live preview of avatar images
- Save/Cancel Actions: Clear action buttons with loading states
- Error Handling: Graceful handling of save failures
- Responsive Design: Works on desktop and mobile
- Accessibility: Screen reader support and keyboard navigation
Accessibility
- Form labels and descriptions
- Keyboard navigation support
- Screen reader announcements
- High contrast support
- Focus indicators on interactive elements
- Error messages properly associated with fields
Best Practices
- Validate Early: Provide immediate feedback on field validation
- Save Often: Consider auto-save for better user experience
- Handle Errors: Gracefully handle network and validation errors
- Image Optimization: Encourage users to use optimized images
- Privacy Guidance: Inform users about data visibility
Troubleshooting
Save Not Working
- Check that
onSave
handler is provided and returns a Promise - Verify network connectivity for API calls
- Check browser console for error messages
Image Preview Not Showing
- Verify image URL is accessible and valid
- Check for CORS issues with external images
- Ensure image format is supported (JPEG, PNG, GIF, WebP)
Validation Errors
- Check that validation rules match component props
- Verify maximum length settings are reasonable
- Ensure required fields are properly marked
Related Components
- ProfileViewer - Display profiles
- ProfileManager - Manage multiple profiles
- ProfilePublisher - Publish to blockchain
API Integration
The component integrates with profile management APIs:
// Update profile
PUT /api/bap/profiles/{id}
// Create profile
POST /api/bap/profiles
// Validate profile data
POST /api/bap/profiles/validate
Notes for Improvement
Enhanced Implementation: The actual component provides more sophisticated features than the basic prompt described:
- Uses complete ProfileInfo type with all schema.org fields
- Proper TypeScript integration with strict typing
- Better character validation and limits
- Multiple visual variants and sizes
- Improved error handling and user feedback
- Real-time image preview with fallback handling