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
Props
Prop | Type | Required | Default | Description |
---|---|---|---|---|
post | PostTransaction | Yes | - | Post transaction data from blockchain |
meta | PostMeta | No | - | Additional post metadata |
signer | BapIdentity | No | - | Current user's identity for interactions |
showActions | boolean | No | true | Show like, reply, share buttons |
showAuthor | boolean | No | true | Show author information |
showTimestamp | boolean | No | true | Show post timestamp |
compact | boolean | No | false | Use compact layout |
onReply | (post: PostTransaction) => void | No | - | Reply button callback |
onShare | (post: PostTransaction) => void | No | - | Share button callback |
onUserClick | (identity: BapIdentity) => void | No | - | Author click callback |
className | string | No | - | 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>
);
}
Trending Posts Widget
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
- Handle Loading States: Show skeleton loaders during data fetching
- Error Boundaries: Gracefully handle post loading errors
- Performance: Use virtualization for large post lists
- User Feedback: Provide immediate feedback for interactions
- Analytics: Track user engagement with posts
- Content Safety: Implement content filtering and reporting
- 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
Related Components
- SocialFeed - Display multiple posts
- PostButton - Create new posts
- LikeButton - Like/unlike posts
- FollowButton - Follow post authors
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