BigBlocks Docs
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

PropTypeRequiredDefaultDescription
profileProfileInfoYes-Current profile data to edit
onSave(updates: Partial<ProfileInfo>) => Promise<void>Yes-Save handler for profile updates
onCancel() => voidNo-Cancel button click handler
allowImageUploadbooleanNotrueEnable image URL input field
maxNameLengthnumberNo50Maximum character limit for name
maxDescriptionLengthnumberNo160Maximum character limit for description
classNamestringNo-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

  1. Validate Early: Provide immediate feedback on field validation
  2. Save Often: Consider auto-save for better user experience
  3. Handle Errors: Gracefully handle network and validation errors
  4. Image Optimization: Encourage users to use optimized images
  5. 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

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