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

View Component Preview →

Installation

npx bigblocks add compact-market-table

Import

import { CompactMarketTable } from 'bigblocks';

Props

PropTypeRequiredDefaultDescription
listingsMarketListing[]Yes-Array of marketplace listings to display
maxItemsnumberNo10Maximum number of items to display
onItemClick(listing: MarketListing) => voidNo-Click handler for listing items
showPricebooleanNotrueDisplay price column
showSellerbooleanNofalseDisplay seller column
showActionsbooleanNotrueShow action buttons (Buy, View)
condensedbooleanNofalseEnable extra compact mode
showCategorybooleanNofalseDisplay category column
showStatusbooleanNofalseDisplay listing status
sortBy'price' | 'date' | 'name'No'date'Default sort order
classNamestringNo-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

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

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}
    />
  );
}