Components/Marketplace
CompactMarketTable
A space-efficient table component for displaying marketplace listings in constrained layouts with optimized mobile responsiveness
A space-efficient table component designed for displaying marketplace listings in constrained layouts like sidebars, mobile views, and dashboard widgets. Provides an optimized view of market data with configurable columns and actions.
Installation
npx bigblocks add compact-market-table
Import
import { CompactMarketTable } from 'bigblocks';
Props
Prop | Type | Required | Default | Description |
---|---|---|---|---|
listings | MarketListing[] | Yes | - | Array of marketplace listings to display |
maxItems | number | No | 10 | Maximum number of items to display |
onItemClick | (listing: MarketListing) => void | No | - | Click handler for listing items |
showPrice | boolean | No | true | Display price column |
showSeller | boolean | No | false | Display seller column |
showActions | boolean | No | true | Show action buttons (Buy, View) |
condensed | boolean | No | false | Enable extra compact mode |
showCategory | boolean | No | false | Display category column |
showStatus | boolean | No | false | Display listing status |
sortBy | 'price' | 'date' | 'name' | No | 'date' | Default sort order |
className | string | No | - | Additional CSS classes |
MarketListing Interface
interface MarketListing {
id: string;
title: string;
description?: string;
price: number; // Price in satoshis
priceBSV?: string; // Formatted BSV price
priceUSD?: number; // USD equivalent
seller: {
address: string;
name?: string;
avatar?: string;
verified?: boolean;
};
category?: string;
status: 'active' | 'sold' | 'pending' | 'expired';
createdAt: number; // Timestamp
updatedAt?: number;
images?: string[];
tags?: string[];
views?: number;
likes?: number;
}
Basic Usage
import { CompactMarketTable } from 'bigblocks';
export default function SidebarListings() {
const recentListings = [
{
id: '1',
title: 'Bitcoin Art NFT',
price: 1000000, // 0.01 BSV
priceBSV: '0.01',
seller: {
address: '1BitcoinArtist...',
name: 'CryptoArtist'
},
category: 'Digital Art',
status: 'active',
createdAt: Date.now()
},
// ... more listings
];
return (
<CompactMarketTable
listings={recentListings}
maxItems={5}
onItemClick={(listing) => {
console.log('Selected listing:', listing.id);
window.location.href = `/listing/${listing.id}`;
}}
/>
);
}
Advanced Usage
Full-Featured Market Widget
import { CompactMarketTable } from 'bigblocks';
import { useState, useEffect } from 'react';
export default function MarketWidget() {
const [listings, setListings] = useState<MarketListing[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState<'all' | 'art' | 'collectibles' | 'services'>('all');
useEffect(() => {
fetchListings();
}, [filter]);
const fetchListings = async () => {
setLoading(true);
try {
const response = await fetch(`/api/market/listings?category=${filter}`);
const data = await response.json();
setListings(data.listings);
} catch (error) {
console.error('Failed to fetch listings:', error);
} finally {
setLoading(false);
}
};
const handleListingClick = (listing: MarketListing) => {
// Track analytics
fetch('/api/analytics/listing-view', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ listingId: listing.id })
});
// Navigate to listing details
window.location.href = `/marketplace/listing/${listing.id}`;
};
const handleQuickBuy = async (listing: MarketListing) => {
if (listing.price > 10000000) { // > 0.1 BSV
if (!confirm(`Buy "${listing.title}" for ${listing.priceBSV} BSV?`)) {
return;
}
}
try {
const response = await fetch('/api/market/quick-buy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ listingId: listing.id })
});
if (response.ok) {
alert('Purchase successful!');
fetchListings(); // Refresh listings
}
} catch (error) {
console.error('Purchase failed:', error);
alert('Purchase failed. Please try again.');
}
};
if (loading) {
return <div className="loading-spinner">Loading marketplace...</div>;
}
return (
<div className="market-widget">
<div className="widget-header">
<h3>Marketplace</h3>
<select
value={filter}
onChange={(e) => setFilter(e.target.value as any)}
className="filter-select"
>
<option value="all">All Categories</option>
<option value="art">Digital Art</option>
<option value="collectibles">Collectibles</option>
<option value="services">Services</option>
</select>
</div>
<CompactMarketTable
listings={listings}
maxItems={8}
showPrice={true}
showSeller={true}
showCategory={true}
showActions={true}
onItemClick={handleListingClick}
className="widget-table"
/>
<div className="widget-footer">
<a href="/marketplace" className="view-all-link">
View All Listings →
</a>
</div>
</div>
);
}
Mobile-Optimized Display
import { CompactMarketTable } from 'bigblocks';
import { useMediaQuery } from 'bigblocks/hooks';
export default function MobileMarketplace() {
const isMobile = useMediaQuery('(max-width: 768px)');
const [selectedListing, setSelectedListing] = useState<MarketListing | null>(null);
const handleItemClick = (listing: MarketListing) => {
if (isMobile) {
// Show modal on mobile instead of navigation
setSelectedListing(listing);
} else {
window.location.href = `/listing/${listing.id}`;
}
};
return (
<div className="mobile-marketplace">
<CompactMarketTable
listings={featuredListings}
maxItems={20}
showPrice={true}
showSeller={!isMobile} // Hide seller on mobile for space
showActions={!isMobile} // Use click handler on mobile
condensed={isMobile}
onItemClick={handleItemClick}
className="mobile-optimized"
/>
{/* Mobile Detail Modal */}
{selectedListing && isMobile && (
<div className="listing-modal">
<div className="modal-content">
<h3>{selectedListing.title}</h3>
<p className="price">{selectedListing.priceBSV} BSV</p>
<p className="seller">by {selectedListing.seller.name || 'Anonymous'}</p>
<div className="modal-actions">
<button onClick={() => buyListing(selectedListing)}>
Buy Now
</button>
<button onClick={() => setSelectedListing(null)}>
Close
</button>
</div>
</div>
</div>
)}
</div>
);
}
Common Patterns
Dashboard Recent Activity
import { CompactMarketTable } from 'bigblocks';
export default function DashboardActivity() {
const { recentListings, popularListings, yourListings } = useMarketData();
return (
<div className="dashboard-activity">
<div className="activity-section">
<h4>🆕 Recent Listings</h4>
<CompactMarketTable
listings={recentListings}
maxItems={5}
showPrice={true}
showActions={false}
condensed={true}
onItemClick={(listing) => navigateToListing(listing.id)}
/>
</div>
<div className="activity-section">
<h4>🔥 Popular Items</h4>
<CompactMarketTable
listings={popularListings}
maxItems={5}
showPrice={true}
showStatus={true}
condensed={true}
sortBy="price"
/>
</div>
<div className="activity-section">
<h4>📦 Your Listings</h4>
<CompactMarketTable
listings={yourListings}
maxItems={5}
showPrice={true}
showStatus={true}
showActions={false}
onItemClick={(listing) => editListing(listing.id)}
/>
</div>
</div>
);
}
Category-Filtered Sidebar
import { CompactMarketTable } from 'bigblocks';
import { useState } from 'react';
interface CategorySidebarProps {
categories: string[];
onCategoryChange?: (category: string) => void;
}
export default function CategorySidebar({ categories, onCategoryChange }: CategorySidebarProps) {
const [selectedCategory, setSelectedCategory] = useState('all');
const [categoryListings, setCategoryListings] = useState<MarketListing[]>([]);
const handleCategorySelect = async (category: string) => {
setSelectedCategory(category);
onCategoryChange?.(category);
// Fetch listings for category
try {
const response = await fetch(`/api/market/category/${category}`);
const data = await response.json();
setCategoryListings(data.listings);
} catch (error) {
console.error('Failed to fetch category listings:', error);
}
};
return (
<div className="category-sidebar">
<div className="category-tabs">
<button
className={selectedCategory === 'all' ? 'active' : ''}
onClick={() => handleCategorySelect('all')}
>
All
</button>
{categories.map((category) => (
<button
key={category}
className={selectedCategory === category ? 'active' : ''}
onClick={() => handleCategorySelect(category)}
>
{category}
</button>
))}
</div>
<CompactMarketTable
listings={categoryListings}
maxItems={10}
showPrice={true}
showSeller={false}
condensed={true}
onItemClick={(listing) => {
console.log('Selected from category:', selectedCategory, listing);
window.location.href = `/marketplace/${selectedCategory}/${listing.id}`;
}}
className="category-listings"
/>
</div>
);
}
Live Auction Feed
import { CompactMarketTable } from 'bigblocks';
import { useEffect, useState } from 'react';
export default function LiveAuctionFeed() {
const [auctions, setAuctions] = useState<MarketListing[]>([]);
const [ws, setWs] = useState<WebSocket | null>(null);
useEffect(() => {
// Connect to auction WebSocket
const websocket = new WebSocket('wss://api.example.com/auctions/live');
websocket.onmessage = (event) => {
const update = JSON.parse(event.data);
if (update.type === 'new_bid') {
setAuctions(prev =>
prev.map(auction =>
auction.id === update.listingId
? { ...auction, price: update.newPrice, priceBSV: update.newPriceBSV }
: auction
)
);
} else if (update.type === 'new_listing') {
setAuctions(prev => [update.listing, ...prev].slice(0, 20));
}
};
setWs(websocket);
return () => {
websocket.close();
};
}, []);
const handleAuctionClick = (listing: MarketListing) => {
// Open auction detail in modal for quick bidding
openAuctionModal(listing);
};
const getTimeRemaining = (endTime: number) => {
const remaining = endTime - Date.now();
const minutes = Math.floor(remaining / 60000);
const seconds = Math.floor((remaining % 60000) / 1000);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
return (
<div className="live-auction-feed">
<div className="feed-header">
<h3>🔴 Live Auctions</h3>
<span className="live-indicator">● LIVE</span>
</div>
<CompactMarketTable
listings={auctions}
maxItems={15}
showPrice={true}
showActions={true}
condensed={false}
onItemClick={handleAuctionClick}
className="auction-table"
/>
<style jsx>{`
.auction-table :global(.price-column) {
color: #10b981;
font-weight: bold;
}
`}</style>
</div>
);
}
Watchlist Widget
import { CompactMarketTable } from 'bigblocks';
import { useWatchlist } from 'bigblocks/hooks';
export default function WatchlistWidget() {
const { watchlist, addToWatchlist, removeFromWatchlist } = useWatchlist();
const [priceAlerts, setPriceAlerts] = useState<Record<string, number>>({});
const handleSetPriceAlert = (listing: MarketListing) => {
const targetPrice = prompt(`Set price alert for "${listing.title}" (current: ${listing.priceBSV} BSV)`);
if (targetPrice) {
const targetSatoshis = parseFloat(targetPrice) * 100000000;
setPriceAlerts(prev => ({
...prev,
[listing.id]: targetSatoshis
}));
// Register price alert
fetch('/api/market/price-alerts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
listingId: listing.id,
targetPrice: targetSatoshis
})
});
}
};
const watchlistWithAlerts = watchlist.map(listing => ({
...listing,
priceAlert: priceAlerts[listing.id],
priceChange: calculatePriceChange(listing)
}));
return (
<div className="watchlist-widget">
<div className="widget-header">
<h3>👁️ Watchlist ({watchlist.length})</h3>
<button onClick={() => window.location.href = '/market/watchlist'}>
Manage
</button>
</div>
{watchlist.length === 0 ? (
<p className="empty-state">No items in watchlist</p>
) : (
<CompactMarketTable
listings={watchlistWithAlerts}
maxItems={10}
showPrice={true}
showStatus={true}
showActions={true}
onItemClick={(listing) => {
console.log('Watchlist item clicked:', listing);
window.location.href = `/listing/${listing.id}`;
}}
className="watchlist-table"
/>
)}
<div className="watchlist-actions">
<button onClick={() => window.location.href = '/marketplace'}>
Browse Marketplace
</button>
</div>
</div>
);
}
Authentication Requirements
The CompactMarketTable can work without authentication for public listings, but some features require auth:
import {
BitcoinAuthProvider,
BitcoinQueryProvider,
CompactMarketTable
} from 'bigblocks';
function App() {
return (
<BitcoinQueryProvider>
<BitcoinAuthProvider config={{ apiUrl: '/api' }}>
{/* Authenticated features: Buy buttons, watchlist, etc. */}
<CompactMarketTable
listings={listings}
showActions={true} // Buy buttons require auth
/>
</BitcoinAuthProvider>
</BitcoinQueryProvider>
);
}
Features
- Space-Efficient Design: Optimized for sidebars and constrained layouts
- Configurable Columns: Show/hide price, seller, category, status as needed
- Mobile Responsive: Automatically adapts to smaller screens
- Action Buttons: Quick buy and view actions (configurable)
- Sorting Options: Sort by price, date, or name
- Condensed Mode: Extra compact display for tight spaces
- Click Handling: Customizable item click behavior
- Real-time Updates: Can integrate with WebSocket for live data
- Performance: Virtualized rendering for large lists
Styling
The component includes responsive styling optimized for different contexts:
/* Default compact table styles */
.compact-market-table {
width: 100%;
font-size: 0.875rem;
}
.compact-market-table th {
padding: 0.5rem;
font-weight: 600;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.compact-market-table td {
padding: 0.5rem;
border-bottom: 1px solid var(--border-color-light);
}
/* Condensed mode */
.compact-market-table.condensed th,
.compact-market-table.condensed td {
padding: 0.25rem 0.5rem;
font-size: 0.813rem;
}
/* Mobile responsive */
@media (max-width: 768px) {
.compact-market-table .seller-column,
.compact-market-table .category-column {
display: none;
}
.compact-market-table .title-column {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
/* Hover effects */
.compact-market-table tbody tr:hover {
background-color: var(--hover-bg);
cursor: pointer;
}
/* Status indicators */
.status-active { color: var(--color-success); }
.status-sold { color: var(--color-muted); }
.status-pending { color: var(--color-warning); }
.status-expired { color: var(--color-error); }
Performance Optimization
Virtual Scrolling for Large Lists
import { CompactMarketTable } from 'bigblocks';
import { useVirtualizer } from '@tanstack/react-virtual';
export default function VirtualizedMarketTable({ listings }: { listings: MarketListing[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: listings.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40, // Estimated row height
overscan: 5,
});
const virtualItems = virtualizer.getVirtualItems();
const virtualListings = virtualItems.map(item => listings[item.index]);
return (
<div ref={parentRef} className="virtual-container" style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
<CompactMarketTable
listings={virtualListings}
maxItems={virtualItems.length}
showPrice={true}
condensed={true}
/>
</div>
</div>
);
}
Troubleshooting
Common Issues
Table not rendering
- Verify listings array is properly formatted
- Check that required props (listings) are provided
- Ensure MarketListing objects have required fields (id, title, price, seller, status)
Click handlers not working
- Verify onItemClick prop is passed correctly
- Check for event propagation issues
- Ensure table rows aren't disabled
Columns not showing/hiding
- Check boolean props (showPrice, showSeller, etc.)
- Verify responsive styles aren't hiding columns
- Test in different viewport sizes
Performance issues with large lists
- Use maxItems to limit displayed rows
- Implement virtual scrolling for lists > 100 items
- Consider pagination or lazy loading
Related Components
- MarketTable - Full-featured market table
- QuickBuyButton - Inline purchase actions
- CreateListingButton - Create new listings
- BuyListingButton - Detailed purchase flow
API Reference
CompactMarketTable Component
interface CompactMarketTableProps {
listings: MarketListing[];
maxItems?: number;
onItemClick?: (listing: MarketListing) => void;
showPrice?: boolean;
showSeller?: boolean;
showActions?: boolean;
condensed?: boolean;
showCategory?: boolean;
showStatus?: boolean;
sortBy?: 'price' | 'date' | 'name';
className?: string;
}
Usage with React Query
import { useQuery } from '@tanstack/react-query';
import { CompactMarketTable } from 'bigblocks';
function MarketTableWithQuery() {
const { data: listings, isLoading } = useQuery({
queryKey: ['market-listings', 'featured'],
queryFn: () => fetch('/api/market/featured').then(res => res.json()),
refetchInterval: 30000, // Refresh every 30 seconds
});
if (isLoading) return <div>Loading listings...</div>;
return (
<CompactMarketTable
listings={listings || []}
maxItems={10}
showPrice={true}
showActions={true}
/>
);
}