BigBlocks Docs
Components/Social

PostCard

Display individual social media posts with author info, content, media, and interaction buttons for on-chain social experiences

A component for displaying individual social media posts from the Bitcoin blockchain with author information, rich content, media support, and interaction capabilities.

Installation

npx bigblocks add post-card

Import

import { PostCard } from 'bigblocks';

Preview

View PostCard examples →

Props

PropTypeRequiredDefaultDescription
postPostTransactionYes-Post transaction data from blockchain
metaPostMetaNo-Additional post metadata
signerBapIdentityNo-Current user's identity for interactions
showActionsbooleanNotrueShow like, reply, share buttons
showAuthorbooleanNotrueShow author information
showTimestampbooleanNotrueShow post timestamp
compactbooleanNofalseUse compact layout
onReply(post: PostTransaction) => voidNo-Reply button callback
onShare(post: PostTransaction) => voidNo-Share button callback
onUserClick(identity: BapIdentity) => voidNo-Author click callback
classNamestringNo-Additional CSS classes

Basic Usage

import { PostCard } from 'bigblocks';

export default function PostDisplay({ post }) {
  const handleReply = (post) => {
    console.log('Replying to post:', post.txid);
    // Open reply composer
  };

  const handleShare = (post) => {
    console.log('Sharing post:', post.txid);
    // Open share dialog
  };

  const handleUserClick = (identity) => {
    console.log('Viewing user:', identity.idKey);
    // Navigate to user profile
  };

  return (
    <PostCard
      post={post}
      onReply={handleReply}
      onShare={handleShare}
      onUserClick={handleUserClick}
    />
  );
}

Advanced Usage

Social Feed Integration

import { PostCard, SocialFeed } from 'bigblocks';
import { useState } from 'react';

export default function SocialFeedWithPosts() {
  const [replyingTo, setReplyingTo] = useState(null);
  const [sharedPost, setSharedPost] = useState(null);

  const handleReply = (post) => {
    setReplyingTo(post);
    // Open reply modal/composer
  };

  const handleShare = (post) => {
    setSharedPost(post);
    // Open share modal with options
  };

  const handleUserClick = (identity) => {
    // Navigate to user profile
    router.push(`/profile/${identity.idKey}`);
  };

  return (
    <div className="social-feed">
      <SocialFeed
        feedType="timeline"
        renderPost={(post, meta) => (
          <PostCard
            key={post.txid}
            post={post}
            meta={meta}
            onReply={handleReply}
            onShare={handleShare}
            onUserClick={handleUserClick}
            className="mb-4 border rounded-lg"
          />
        )}
      />
      
      {replyingTo && (
        <ReplyModal
          post={replyingTo}
          onClose={() => setReplyingTo(null)}
        />
      )}
      
      {sharedPost && (
        <ShareModal
          post={sharedPost}
          onClose={() => setSharedPost(null)}
        />
      )}
    </div>
  );
}

Thread View Display

import { PostCard } from 'bigblocks';

export default function ThreadView({ thread }) {
  const { rootPost, replies, currentUser } = thread;

  return (
    <div className="thread-view">
      {/* Main post */}
      <div className="main-post">
        <PostCard
          post={rootPost}
          meta={rootPost.meta}
          signer={currentUser}
          onReply={(post) => openReplyComposer(post)}
          onShare={(post) => sharePost(post)}
          onUserClick={(identity) => viewProfile(identity)}
          className="border-2 border-orange-200 bg-orange-50"
        />
      </div>
      
      {/* Thread replies */}
      <div className="thread-replies">
        {replies.map((reply, index) => (
          <div 
            key={reply.txid}
            className="ml-8 border-l-2 border-gray-200 pl-4 mt-4"
            style={{ marginLeft: `${reply.depth * 32}px` }}
          >
            <PostCard
              post={reply}
              meta={reply.meta}
              signer={currentUser}
              compact={reply.depth > 2}
              onReply={(post) => openReplyComposer(post)}
              onUserClick={(identity) => viewProfile(identity)}
            />
          </div>
        ))}
      </div>
    </div>
  );
}

User Profile Posts

import { PostCard } from 'bigblocks';

export default function UserProfilePosts({ userPosts, profileOwner, currentUser }) {
  return (
    <div className="user-posts">
      <h3>Posts by {profileOwner.name}</h3>
      
      <div className="posts-list">
        {userPosts.map(post => (
          <PostCard
            key={post.txid}
            post={post}
            meta={post.meta}
            signer={currentUser}
            showAuthor={false} // Hide author since it's the profile owner
            onReply={(post) => {
              // Handle reply to user's post
              openReplyDialog(post, profileOwner);
            }}
            onShare={(post) => {
              // Handle sharing user's post
              shareUserPost(post, profileOwner);
            }}
            className="mb-6 border-b pb-6"
          />
        ))}
      </div>
    </div>
  );
}

Compact Post Cards

import { PostCard } from 'bigblocks';

export default function CompactPostList({ posts }) {
  return (
    <div className="compact-posts">
      <h3>Recent Activity</h3>
      
      <div className="space-y-2">
        {posts.map(post => (
          <PostCard
            key={post.txid}
            post={post}
            compact={true}
            showActions={false}
            onUserClick={(identity) => {
              // Navigate to user profile
              router.push(`/users/${identity.idKey}`);
            }}
            className="p-3 hover:bg-gray-50 rounded"
          />
        ))}
      </div>
    </div>
  );
}

Post with Rich Media

import { PostCard } from 'bigblocks';

export default function MediaPostDisplay({ mediaPost }) {
  const handleMediaClick = (media) => {
    // Open media in fullscreen viewer
    openMediaViewer(media);
  };

  return (
    <div className="media-post">
      <PostCard
        post={mediaPost}
        meta={{
          ...mediaPost.meta,
          media: mediaPost.meta.media?.map(item => ({
            ...item,
            onClick: () => handleMediaClick(item)
          }))
        }}
        onReply={(post) => {
          // Reply to media post
          openReplyWithMedia(post);
        }}
        onShare={(post) => {
          // Share media post
          shareMediaPost(post);
        }}
        className="media-post-card"
      />
    </div>
  );
}

Post Analytics Integration

import { PostCard } from 'bigblocks';
import { useState, useEffect } from 'react';

export default function PostWithAnalytics({ post, showAnalytics = false }) {
  const [analytics, setAnalytics] = useState(null);
  const [loading, setLoading] = useState(false);

  const loadAnalytics = async () => {
    if (!showAnalytics) return;
    
    setLoading(true);
    try {
      const data = await fetchPostAnalytics(post.txid);
      setAnalytics(data);
    } catch (error) {
      console.error('Failed to load analytics:', error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    loadAnalytics();
  }, [post.txid, showAnalytics]);

  return (
    <div className="post-with-analytics">
      <PostCard
        post={post}
        onReply={(post) => replyToPost(post)}
        onShare={(post) => sharePost(post)}
        className="border rounded-lg"
      />
      
      {showAnalytics && (
        <div className="analytics-section mt-4 p-4 bg-gray-50 rounded">
          {loading ? (
            <div>Loading analytics...</div>
          ) : analytics ? (
            <div className="analytics-grid">
              <div className="stat">
                <span className="label">Views</span>
                <span className="value">{analytics.views}</span>
              </div>
              <div className="stat">
                <span className="label">Engagement</span>
                <span className="value">{analytics.engagementRate}%</span>
              </div>
              <div className="stat">
                <span className="label">Reach</span>
                <span className="value">{analytics.reach}</span>
              </div>
            </div>
          ) : (
            <div>Analytics not available</div>
          )}
        </div>
      )}
    </div>
  );
}

Interactive Post Actions

import { PostCard, LikeButton, FollowButton } from 'bigblocks';
import { useState } from 'react';

export default function InteractivePost({ post, currentUser }) {
  const [isLiked, setIsLiked] = useState(false);
  const [likeCount, setLikeCount] = useState(post.meta?.likes || 0);
  const [isFollowing, setIsFollowing] = useState(false);

  const handleLike = async () => {
    try {
      if (isLiked) {
        await unlikePost(post.txid);
        setLikeCount(prev => prev - 1);
      } else {
        await likePost(post.txid);
        setLikeCount(prev => prev + 1);
      }
      setIsLiked(!isLiked);
    } catch (error) {
      console.error('Like action failed:', error);
    }
  };

  const handleFollow = async () => {
    try {
      if (isFollowing) {
        await unfollowUser(post.bapId);
      } else {
        await followUser(post.bapId);
      }
      setIsFollowing(!isFollowing);
    } catch (error) {
      console.error('Follow action failed:', error);
    }
  };

  return (
    <div className="interactive-post">
      <PostCard
        post={post}
        signer={currentUser}
        onReply={(post) => {
          // Open reply composer
          openReplyComposer(post);
        }}
        onShare={(post) => {
          // Share post
          sharePost(post);
        }}
        onUserClick={(identity) => {
          // View user profile
          router.push(`/profile/${identity.idKey}`);
        }}
      />
      
      {/* Custom action bar */}
      <div className="custom-actions flex justify-between items-center mt-4 p-3 border-t">
        <div className="left-actions flex space-x-4">
          <LikeButton
            txid={post.txid}
            isLiked={isLiked}
            count={likeCount}
            onLike={handleLike}
          />
          
          <button
            onClick={() => openReplyComposer(post)}
            className="text-gray-500 hover:text-blue-500"
          >
            💬 Reply
          </button>
          
          <button
            onClick={() => sharePost(post)}
            className="text-gray-500 hover:text-green-500"
          >
            🔄 Share
          </button>
        </div>
        
        <div className="right-actions">
          {post.bapId !== currentUser?.idKey && (
            <FollowButton
              idKey={post.bapId}
              isFollowing={isFollowing}
              onFollow={handleFollow}
              onUnfollow={handleFollow}
              size="small"
            />
          )}
        </div>
      </div>
    </div>
  );
}

Post Transaction Structure

interface PostTransaction {
  txid: string;                    // Transaction ID on blockchain
  timestamp: number;               // Post timestamp
  bapId: string;                   // Author's BAP identity
  content: string;                 // Post content
  app: string;                     // App identifier (e.g., 'bSocial')
  type: 'post';                    // Transaction type
  context?: string;                // Reply context
  // Additional fields from bmap-api-types
}

interface PostMeta {
  likes?: number;                  // Like count
  replies?: number;                // Reply count
  reposts?: number;                // Repost count
  media?: MediaItem[];             // Attached media
  mentions?: Mention[];            // User mentions
  hashtags?: Hashtag[];           // Hashtags
  links?: LinkPreview[];          // Link previews
}

interface BapIdentity {
  idKey: string;                   // BAP identity key
  name?: string;                   // Display name
  avatar?: string;                 // Avatar URL
  // Additional identity fields
}

Features

Content Display

  • Rich Text: Markdown support with proper formatting
  • Media Support: Images, videos, and embedded content
  • Link Previews: Automatic link metadata display
  • Mentions: Clickable user mentions
  • Hashtags: Clickable hashtag links

Author Information

  • Avatar Display: User profile pictures with fallbacks
  • Verification Badges: Verified user indicators
  • User Names: Display names and handles
  • Profile Links: Clickable author information

Interaction Features

  • Like/Unlike: Heart button with counts
  • Reply: Comment on posts
  • Share/Repost: Share posts with others
  • Follow: Follow post authors
  • User Profiles: Navigate to author profiles

Layout Options

  • Standard: Full-featured post display
  • Compact: Condensed layout for lists
  • Thread: Nested conversation view
  • Media-focused: Emphasis on visual content

Common Patterns

Post Feed Component

import { PostCard } from 'bigblocks';

export default function PostFeed({ posts, currentUser }) {
  const [interactions, setInteractions] = useState({});

  const updateInteraction = (postId, type, value) => {
    setInteractions(prev => ({
      ...prev,
      [postId]: {
        ...prev[postId],
        [type]: value
      }
    }));
  };

  return (
    <div className="post-feed space-y-6">
      {posts.map(post => (
        <PostCard
          key={post.txid}
          post={post}
          meta={{
            ...post.meta,
            ...interactions[post.txid]
          }}
          signer={currentUser}
          onReply={(post) => {
            openReplyDialog(post);
          }}
          onShare={(post) => {
            sharePostDialog(post);
          }}
          onUserClick={(identity) => {
            router.push(`/profile/${identity.idKey}`);
          }}
          className="bg-white border rounded-lg shadow-sm hover:shadow-md transition-shadow"
        />
      ))}
    </div>
  );
}

Search Results Display

import { PostCard } from 'bigblocks';

export default function SearchResults({ searchResults, query }) {
  return (
    <div className="search-results">
      <h2>Posts matching "{query}"</h2>
      
      <div className="results-list">
        {searchResults.posts.map(post => (
          <PostCard
            key={post.txid}
            post={post}
            meta={post.meta}
            showTimestamp={true}
            onUserClick={(identity) => {
              // Track search result click
              analytics.track('search_result_click', {
                query,
                postId: post.txid,
                authorId: identity.idKey
              });
              
              router.push(`/profile/${identity.idKey}`);
            }}
            className="mb-4 p-4 border rounded hover:bg-gray-50"
          />
        ))}
      </div>
    </div>
  );
}
import { PostCard } from 'bigblocks';

export default function TrendingPosts({ trendingPosts }) {
  return (
    <div className="trending-posts">
      <h3 className="font-bold mb-4">🔥 Trending Now</h3>
      
      <div className="space-y-3">
        {trendingPosts.slice(0, 5).map((post, index) => (
          <div key={post.txid} className="trending-item">
            <div className="flex items-center space-x-2 mb-2">
              <span className="trending-rank">#{index + 1}</span>
              <span className="trending-score">
                {post.meta.trendingScore} points
              </span>
            </div>
            
            <PostCard
              post={post}
              meta={post.meta}
              compact={true}
              showActions={false}
              onUserClick={(identity) => {
                router.push(`/profile/${identity.idKey}`);
              }}
              className="bg-gradient-to-r from-orange-50 to-yellow-50 border border-orange-200 rounded"
            />
          </div>
        ))}
      </div>
    </div>
  );
}

Styling and Customization

Custom Styling

import { PostCard } from 'bigblocks';

export default function StyledPostCard({ post }) {
  return (
    <PostCard
      post={post}
      className="
        bg-white 
        border-2 border-orange-200 
        rounded-xl 
        shadow-lg 
        hover:shadow-xl 
        transition-all 
        duration-300 
        hover:scale-105
      "
      onReply={(post) => handleReply(post)}
      onShare={(post) => handleShare(post)}
    />
  );
}

Dark Mode Support

import { PostCard } from 'bigblocks';

export default function ThemedPostCard({ post, darkMode }) {
  return (
    <PostCard
      post={post}
      className={`
        border rounded-lg transition-colors
        ${darkMode 
          ? 'bg-gray-800 border-gray-700 text-white' 
          : 'bg-white border-gray-200 text-gray-900'
        }
      `}
      onReply={(post) => handleReply(post)}
      onShare={(post) => handleShare(post)}
    />
  );
}

Performance Optimization

Virtualized Posts

import { PostCard } from 'bigblocks';
import { FixedSizeList as List } from 'react-window';

export default function VirtualizedPostFeed({ posts }) {
  const PostItem = ({ index, style }) => {
    const post = posts[index];
    
    return (
      <div style={style}>
        <PostCard
          post={post}
          meta={post.meta}
          onReply={(post) => handleReply(post)}
          onShare={(post) => handleShare(post)}
          className="mx-4 mb-4"
        />
      </div>
    );
  };

  return (
    <List
      height={600}
      itemCount={posts.length}
      itemSize={200}
      itemData={posts}
    >
      {PostItem}
    </List>
  );
}

Lazy Loading

import { PostCard } from 'bigblocks';
import { useInView } from 'react-intersection-observer';

export default function LazyPostCard({ post, onVisible }) {
  const { ref, inView } = useInView({
    threshold: 0.1,
    triggerOnce: true,
    onChange: (inView) => {
      if (inView) {
        onVisible?.(post);
      }
    }
  });

  return (
    <div ref={ref}>
      {inView ? (
        <PostCard
          post={post}
          onReply={(post) => handleReply(post)}
          onShare={(post) => handleShare(post)}
        />
      ) : (
        <div className="post-skeleton">
          <div className="animate-pulse bg-gray-200 h-32 rounded" />
        </div>
      )}
    </div>
  );
}

Accessibility

  • Semantic HTML: Proper article and header structure
  • ARIA Labels: Screen reader support for actions
  • Keyboard Navigation: Full keyboard accessibility
  • Focus Management: Proper focus indicators
  • High Contrast: Support for high contrast modes
  • Screen Readers: Descriptive content for assistive technology

Best Practices

  1. Handle Loading States: Show skeleton loaders during data fetching
  2. Error Boundaries: Gracefully handle post loading errors
  3. Performance: Use virtualization for large post lists
  4. User Feedback: Provide immediate feedback for interactions
  5. Analytics: Track user engagement with posts
  6. Content Safety: Implement content filtering and reporting
  7. Real-time Updates: Update post stats in real-time when possible

Troubleshooting

Posts Not Displaying

  • Check that post data structure matches PostTransaction interface
  • Verify blockchain connection for on-chain posts
  • Ensure proper authentication for restricted content

Interaction Buttons Not Working

  • Verify signer prop is provided for authenticated actions
  • Check that callback functions are properly implemented
  • Ensure wallet connectivity for blockchain transactions

Media Not Loading

  • Check media URL accessibility
  • Verify CORS settings for external media
  • Implement fallback for failed media loads

API Integration

The component integrates with bSocial protocol APIs:

// Get post details
GET /api/social/posts/{txid}

// Get post metadata
GET /api/social/posts/{txid}/meta

// Post interactions
POST /api/social/posts/{txid}/like
POST /api/social/posts/{txid}/reply
POST /api/social/posts/{txid}/share

Notes for Improvement

Enhanced Implementation: The actual component provides more sophisticated features than the basic prompt described:

  • Full integration with bmap-api-types for proper blockchain data structure
  • Support for rich media display with lazy loading
  • Real-time interaction capabilities with like/reply/share
  • Advanced thread display with proper nesting
  • Better performance optimizations for large feeds
  • Comprehensive accessibility support
  • Integration with BAP identity system for author verification