Components/Marketplace
CreateListingButton
Create marketplace listings on the Bitcoin blockchain with customizable forms, image uploads, and rich metadata
A comprehensive component for creating marketplace listings on the Bitcoin blockchain, featuring customizable forms, multi-image uploads, category selection, and on-chain metadata storage for permanent, decentralized marketplace functionality.
Installation
npx bigblocks add create-listing-button
Import
import { CreateListingButton } from 'bigblocks';
Props
Prop | Type | Required | Default | Description |
---|---|---|---|---|
onCreateListing | (listing: Listing) => void | No | - | Callback when listing is created successfully |
onError | (error: MarketError) => void | No | - | Error callback |
categories | string[] | No | Default categories | Available categories for selection |
maxImages | number | No | 5 | Maximum number of images allowed |
requiresApproval | boolean | No | false | Whether listings require admin approval |
feeAmount | number | No | 0.001 | Listing fee in BSV |
variant | 'button' | 'card' | 'inline' | No | 'button' | Display variant |
size | 'sm' | 'md' | 'lg' | No | 'md' | Component size |
className | string | No | - | Additional CSS classes |
Listing Interface
interface Listing {
id: string;
txid: string; // Blockchain transaction ID
title: string;
description: string;
price: number; // Price in satoshis
currency: 'BSV' | 'USD';
category: string;
images: string[]; // Array of image URLs
seller: {
address: string;
name?: string;
idKey: string; // BAP identity key
};
assetType: 'BSV20' | 'BSV21' | 'BSV721' | 'DIGITAL' | 'PHYSICAL';
metadata?: {
condition?: 'new' | 'used' | 'refurbished';
location?: string;
shipping?: boolean;
tags?: string[];
};
paymentAddress: string; // Address to receive payment
ordAddress: string; // Ordinals address for NFTs
timestamp: number;
status: 'active' | 'sold' | 'cancelled';
}
interface MarketError {
code:
| 'INSUFFICIENT_FUNDS'
| 'INVALID_LISTING_DATA'
| 'IMAGE_TOO_LARGE'
| 'UNSUPPORTED_ASSET_TYPE'
| 'CATEGORY_NOT_FOUND'
| 'RATE_LIMITED';
message: string;
details?: {
required?: number;
available?: number;
maxSize?: number;
supportedTypes?: string[];
};
}
Basic Usage
import { CreateListingButton } from 'bigblocks';
export default function Marketplace() {
const handleListingCreated = (listing: Listing) => {
console.log('Created listing:', listing);
console.log('Transaction ID:', listing.txid);
// Navigate to listing detail page
window.location.href = `/marketplace/listing/${listing.id}`;
};
const handleError = (error: MarketError) => {
console.error('Listing creation failed:', error);
if (error.code === 'INSUFFICIENT_FUNDS') {
alert(`Need ${error.details?.required} satoshis, but only have ${error.details?.available}`);
} else {
alert(error.message);
}
};
return (
<CreateListingButton
onCreateListing={handleListingCreated}
onError={handleError}
/>
);
}
Advanced Usage
Complete Marketplace Integration
import { CreateListingButton } from 'bigblocks';
import { useState, useEffect } from 'react';
export default function AdvancedMarketplace() {
const [categories, setCategories] = useState<string[]>([]);
const [userListings, setUserListings] = useState<Listing[]>([]);
const [listingFee, setListingFee] = useState(0.001);
useEffect(() => {
// Fetch available categories
fetchCategories();
// Get current listing fee
fetchListingFee();
}, []);
const fetchCategories = async () => {
try {
const response = await fetch('/api/market/categories');
const data = await response.json();
setCategories(data.categories.map((cat: any) => cat.name));
} catch (error) {
console.error('Failed to fetch categories:', error);
}
};
const fetchListingFee = async () => {
try {
const response = await fetch('/api/market/listings/estimate-fee', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
assetType: 'DIGITAL',
imageCount: 1,
hasMetadata: true
})
});
const data = await response.json();
setListingFee(data.fee / 100000000); // Convert to BSV
} catch (error) {
console.error('Failed to fetch fee:', error);
}
};
const handleListingCreated = async (listing: Listing) => {
console.log('New listing created:', listing);
// Add to user's listings
setUserListings(prev => [listing, ...prev]);
// Track analytics
await fetch('/api/analytics/listing-created', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
listingId: listing.id,
category: listing.category,
price: listing.price,
timestamp: Date.now()
})
});
// Show success notification
const notification = new Notification('Listing Created!', {
body: `Your listing "${listing.title}" is now live on the marketplace.`,
icon: '/icon-192x192.png'
});
// Navigate to listing
setTimeout(() => {
window.location.href = `/marketplace/listing/${listing.id}`;
}, 2000);
};
return (
<div className="advanced-marketplace">
<div className="marketplace-header">
<h1>Create New Listing</h1>
<p className="fee-info">
Listing fee: {listingFee} BSV
</p>
</div>
<CreateListingButton
categories={categories}
maxImages={10}
feeAmount={listingFee}
variant="card"
size="lg"
onCreateListing={handleListingCreated}
onError={(error) => {
if (error.code === 'RATE_LIMITED') {
alert('You can only create 10 listings per hour. Please try again later.');
} else if (error.code === 'IMAGE_TOO_LARGE') {
alert(`Image too large. Maximum size: ${error.details?.maxSize} MB`);
} else {
alert(`Error: ${error.message}`);
}
}}
className="marketplace-create-button"
/>
{userListings.length > 0 && (
<div className="user-listings mt-8">
<h2>Your Recent Listings</h2>
<div className="listings-grid">
{userListings.slice(0, 6).map((listing) => (
<div key={listing.id} className="listing-card">
<img src={listing.images[0]} alt={listing.title} />
<h3>{listing.title}</h3>
<p className="price">{listing.price / 100000000} BSV</p>
<span className="status">{listing.status}</span>
</div>
))}
</div>
</div>
)}
</div>
);
}
NFT Marketplace Creation
import { CreateListingButton } from 'bigblocks';
interface NFTMetadata {
name: string;
description: string;
image: string;
attributes: Array<{
trait_type: string;
value: string | number;
}>;
rarity?: string;
collection?: string;
}
export default function NFTMarketplace() {
const handleNFTListing = async (listing: Listing) => {
console.log('NFT listing created:', listing);
// For NFT listings, the ordAddress is particularly important
console.log('Ordinals address:', listing.ordAddress);
console.log('Asset type:', listing.assetType); // BSV721
// Store NFT metadata on-chain
await storeNFTMetadata(listing.id, listing.metadata as NFTMetadata);
};
return (
<div className="nft-marketplace">
<h1>List Your NFT</h1>
<CreateListingButton
variant="card"
categories={['Art', 'Collectibles', 'Gaming', 'Music', 'Photography']}
maxImages={10}
feeAmount={0.01} // Higher fee for NFT listings
onCreateListing={handleNFTListing}
className="nft-create-listing"
/>
<div className="listing-tips">
<h3>Tips for NFT Listings</h3>
<ul>
<li>Use high-quality images (at least 1000x1000px)</li>
<li>Include detailed metadata and attributes</li>
<li>Specify rarity and collection information</li>
<li>Set competitive prices based on market trends</li>
</ul>
</div>
</div>
);
}
Common Patterns
Multi-Step Listing Creation
import { CreateListingButton } from 'bigblocks';
import { useState } from 'react';
export default function MultiStepListing() {
const [step, setStep] = useState<'prepare' | 'create' | 'complete'>('prepare');
const [preparedData, setPreparedData] = useState<any>(null);
const [createdListing, setCreatedListing] = useState<Listing | null>(null);
const handlePrepare = () => {
// Prepare listing data
const data = {
title: document.getElementById('title')?.value,
description: document.getElementById('description')?.value,
price: parseFloat(document.getElementById('price')?.value) * 100000000,
category: document.getElementById('category')?.value,
};
setPreparedData(data);
setStep('create');
};
const handleListingCreated = (listing: Listing) => {
setCreatedListing(listing);
setStep('complete');
};
return (
<div className="multi-step-listing">
{step === 'prepare' && (
<div className="step-prepare">
<h2>Prepare Your Listing</h2>
<form>
<input id="title" placeholder="Title" required />
<textarea id="description" placeholder="Description" required />
<input id="price" type="number" placeholder="Price in BSV" required />
<select id="category" required>
<option value="">Select Category</option>
<option value="Electronics">Electronics</option>
<option value="Art">Art</option>
<option value="Collectibles">Collectibles</option>
</select>
</form>
<button onClick={handlePrepare}>Next: Add Images</button>
</div>
)}
{step === 'create' && (
<div className="step-create">
<h2>Complete Your Listing</h2>
<div className="prepared-info">
<h3>{preparedData.title}</h3>
<p>{preparedData.description}</p>
<p className="price">{preparedData.price / 100000000} BSV</p>
</div>
<CreateListingButton
variant="inline"
onCreateListing={handleListingCreated}
className="complete-listing-button"
/>
</div>
)}
{step === 'complete' && createdListing && (
<div className="step-complete">
<h2>✅ Listing Created Successfully!</h2>
<div className="listing-summary">
<h3>{createdListing.title}</h3>
<p>Transaction ID: {createdListing.txid}</p>
<p>Listing ID: {createdListing.id}</p>
<p>Status: {createdListing.status}</p>
</div>
<div className="next-actions">
<button onClick={() => window.location.href = `/marketplace/listing/${createdListing.id}`}>
View Listing
</button>
<button onClick={() => window.location.href = '/marketplace/dashboard'}>
Go to Dashboard
</button>
<button onClick={() => setStep('prepare')}>
Create Another
</button>
</div>
</div>
)}
</div>
);
}
Bulk Listing Upload
import { CreateListingButton } from 'bigblocks';
import { useState } from 'react';
interface BulkListingData {
title: string;
description: string;
price: number;
category: string;
images: File[];
}
export default function BulkListingUpload() {
const [bulkData, setBulkData] = useState<BulkListingData[]>([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [created, setCreated] = useState<Listing[]>([]);
const handleCSVUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
// Parse CSV and populate bulkData
const reader = new FileReader();
reader.onload = (e) => {
const csv = e.target?.result as string;
const parsed = parseCSV(csv);
setBulkData(parsed);
};
reader.readAsText(file);
};
const handleListingCreated = (listing: Listing) => {
setCreated(prev => [...prev, listing]);
// Move to next listing
if (currentIndex < bulkData.length - 1) {
setCurrentIndex(currentIndex + 1);
} else {
console.log('All listings created!', created);
}
};
const currentListing = bulkData[currentIndex];
return (
<div className="bulk-listing-upload">
<h2>Bulk Listing Upload</h2>
{bulkData.length === 0 ? (
<div className="upload-section">
<input
type="file"
accept=".csv"
onChange={handleCSVUpload}
/>
<p>Upload a CSV file with your listings</p>
</div>
) : (
<div className="bulk-creation">
<div className="progress">
Creating listing {currentIndex + 1} of {bulkData.length}
</div>
<div className="current-listing">
<h3>{currentListing.title}</h3>
<p>{currentListing.description}</p>
<p>{currentListing.price} BSV</p>
</div>
<CreateListingButton
variant="inline"
onCreateListing={handleListingCreated}
onError={(error) => {
console.error(`Failed to create listing ${currentIndex + 1}:`, error);
// Skip to next
if (currentIndex < bulkData.length - 1) {
setCurrentIndex(currentIndex + 1);
}
}}
/>
<div className="created-count">
✅ {created.length} listings created successfully
</div>
</div>
)}
</div>
);
}
Service Marketplace
import { CreateListingButton } from 'bigblocks';
interface ServiceListing extends Listing {
metadata: {
serviceType: 'consultation' | 'development' | 'design' | 'writing' | 'other';
deliveryTime: string;
revisions: number;
requirements?: string[];
};
}
export default function ServiceMarketplace() {
const handleServiceListing = (listing: Listing) => {
const serviceListing = listing as ServiceListing;
console.log('Service listing created:', {
title: serviceListing.title,
type: serviceListing.metadata.serviceType,
delivery: serviceListing.metadata.deliveryTime,
price: `${serviceListing.price / 100000000} BSV`
});
};
return (
<div className="service-marketplace">
<h1>Offer Your Services</h1>
<CreateListingButton
categories={[
'Consultation',
'Development',
'Design',
'Writing',
'Marketing',
'Other Services'
]}
variant="card"
size="lg"
onCreateListing={handleServiceListing}
className="service-listing-button"
/>
<div className="service-examples">
<h3>Popular Services</h3>
<ul>
<li>🖥️ Smart Contract Development</li>
<li>🎨 NFT Art Creation</li>
<li>✍️ Technical Writing</li>
<li>📊 Blockchain Consulting</li>
<li>🔍 Security Audits</li>
</ul>
</div>
</div>
);
}
Physical Goods with Shipping
import { CreateListingButton } from 'bigblocks';
export default function PhysicalGoodsMarketplace() {
const shippingRegions = ['Local', 'National', 'International'];
const handlePhysicalListing = async (listing: Listing) => {
if (listing.assetType !== 'PHYSICAL') return;
// Calculate shipping costs
const shippingCosts = await calculateShipping({
weight: listing.metadata?.weight,
dimensions: listing.metadata?.dimensions,
destination: listing.metadata?.location
});
console.log('Physical item listed:', {
title: listing.title,
shipping: listing.metadata?.shipping,
location: listing.metadata?.location,
estimatedShipping: shippingCosts
});
};
return (
<div className="physical-goods-marketplace">
<h1>Sell Physical Items</h1>
<CreateListingButton
categories={[
'Electronics',
'Books',
'Collectibles',
'Art',
'Jewelry',
'Home & Garden',
'Sports & Outdoors'
]}
variant="card"
maxImages={10}
onCreateListing={handlePhysicalListing}
/>
<div className="shipping-info">
<h3>Shipping Information</h3>
<p>When listing physical items, please include:</p>
<ul>
<li>Item weight and dimensions</li>
<li>Shipping regions you support</li>
<li>Estimated delivery times</li>
<li>Return policy</li>
</ul>
</div>
</div>
);
}
Authentication Requirements
The CreateListingButton requires Bitcoin authentication for creating listings:
import {
BitcoinAuthProvider,
BitcoinQueryProvider,
CreateListingButton
} from 'bigblocks';
function App() {
return (
<BitcoinQueryProvider>
<BitcoinAuthProvider config={{
apiUrl: '/api',
walletMode: 'integrated' // Required for payment
}}>
<CreateListingButton
onCreateListing={(listing) => {
console.log('Created listing:', listing);
}}
/>
</BitcoinAuthProvider>
</BitcoinQueryProvider>
);
}
API Integration
The CreateListingButton integrates with several backend endpoints:
Core Listing Endpoints
// Create new listing
POST /api/market/listings
{
title: string;
description: string;
price: number; // satoshis
currency: 'BSV' | 'USD';
category: string;
images: string[];
metadata?: object;
assetType: string;
}
// Upload images
POST /api/market/images/upload
FormData with multiple image files
// Get categories
GET /api/market/categories
// Estimate listing fee
POST /api/market/listings/estimate-fee
{
assetType: string;
imageCount: number;
hasMetadata: boolean;
}
Real-time Updates
// Server-Sent Events for marketplace
GET /api/market/stream
Events:
- listing_created: { listing: Listing }
- listing_sold: { listingId: string, buyer: string }
- listing_cancelled: { listingId: string }
- price_updated: { listingId: string, newPrice: number }
Features
- On-chain Storage: Permanent blockchain marketplace listings
- Multi-Image Upload: Support for multiple images with automatic thumbnails
- Category System: Organized hierarchical category structure
- Flexible Pricing: Support for BSV or USD pricing
- Rich Metadata: Detailed item information and attributes
- Preview Mode: Review listing before blockchain submission
- Fee Estimation: Calculate listing fees before creation
- Draft Saving: Save listings as drafts before publishing
- Asset Types: Support for digital goods, NFTs, and physical items
Error Handling
Comprehensive Error Management
import { CreateListingButton } from 'bigblocks';
export default function RobustListingCreation() {
const handleError = (error: MarketError) => {
switch (error.code) {
case 'INSUFFICIENT_FUNDS':
console.log(`Need ${error.details?.required} satoshis for listing fee`);
// Prompt user to add funds
showFundingModal(error.details?.required);
break;
case 'IMAGE_TOO_LARGE':
alert(`Image exceeds maximum size of ${error.details?.maxSize} MB`);
break;
case 'RATE_LIMITED':
alert('You have reached the hourly listing limit. Please try again later.');
break;
case 'UNSUPPORTED_ASSET_TYPE':
console.error('Asset type not supported:', error.details?.supportedTypes);
break;
default:
alert(`Error creating listing: ${error.message}`);
}
};
return (
<CreateListingButton
onError={handleError}
onCreateListing={(listing) => {
console.log('Success:', listing);
}}
/>
);
}
Styling
The component includes multiple display variants:
/* Button variant */
.create-listing-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background: var(--primary-color);
color: white;
border-radius: 0.5rem;
font-weight: 600;
transition: all 0.2s;
}
/* Card variant */
.create-listing-card {
border: 1px solid var(--border-color);
border-radius: 0.75rem;
padding: 2rem;
background: var(--card-background);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Inline variant */
.create-listing-inline {
width: 100%;
padding: 1rem;
border: 2px dashed var(--border-color);
border-radius: 0.5rem;
text-align: center;
cursor: pointer;
transition: border-color 0.2s;
}
.create-listing-inline:hover {
border-color: var(--primary-color);
}
/* Modal form */
.listing-form-modal {
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
}
/* Image upload area */
.image-upload-zone {
border: 2px dashed var(--border-color);
border-radius: 0.5rem;
padding: 2rem;
text-align: center;
cursor: pointer;
}
.image-preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
Performance Optimization
Image Optimization
import { CreateListingButton } from 'bigblocks';
export default function OptimizedImageUpload() {
const processImages = async (images: File[]) => {
// Resize and compress images before upload
const optimized = await Promise.all(
images.map(async (file) => {
if (file.size > 1024 * 1024) { // > 1MB
return compressImage(file, {
maxWidth: 1920,
maxHeight: 1080,
quality: 0.8
});
}
return file;
})
);
return optimized;
};
return (
<CreateListingButton
maxImages={20} // Allow more images since they're optimized
onCreateListing={(listing) => {
console.log('Optimized listing created:', listing);
}}
/>
);
}
Troubleshooting
Common Issues
Form validation errors
- Ensure all required fields are filled
- Check price is a valid number > 0
- Verify at least one image is uploaded
- Confirm category is selected
Image upload failures
- Check file size (max 10MB per image)
- Verify supported formats (JPG, PNG, GIF, WebP)
- Ensure stable internet connection
- Try reducing image dimensions
Insufficient funds error
- Check wallet balance for listing fee
- Fee includes base fee + image storage
- Typical fee: 0.001-0.01 BSV
Rate limiting
- Maximum 10 listings per hour
- Wait for cooldown period
- Consider bulk upload for multiple items
Related Components
- CompactMarketTable - Display marketplace listings
- MarketTable - Full marketplace table
- BuyListingButton - Purchase listings
- QuickBuyButton - Quick purchase interface
API Reference
CreateListingButton Component
interface CreateListingButtonProps {
onCreateListing?: (listing: Listing) => void;
onError?: (error: MarketError) => void;
categories?: string[];
maxImages?: number;
requiresApproval?: boolean;
feeAmount?: number;
variant?: 'button' | 'card' | 'inline';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
Usage with React Query
import { useMutation } from '@tanstack/react-query';
import { CreateListingButton } from 'bigblocks';
function CreateListingWithQuery() {
const createMutation = useMutation({
mutationFn: (listingData: any) =>
fetch('/api/market/listings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(listingData)
}).then(res => res.json()),
onSuccess: (data) => {
console.log('Listing created:', data.listing);
queryClient.invalidateQueries(['market-listings']);
}
});
return (
<CreateListingButton
onCreateListing={(listing) => {
console.log('Component listing:', listing);
// Additional processing if needed
}}
/>
);
}