UNPKG

bigblocks

Version:

Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React

17 lines 12.3 kB
{ "name": "social-components-socialfeed", "type": "registry:component", "dependencies": [ "bmap-api-types" ], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/social/components/SocialFeed.tsx", "type": "registry:component", "content": "\"use client\";\n\nimport type { BapIdentity, PostsResponse } from \"bmap-api-types\";\nimport { useState } from \"react\";\nimport { responsive } from \"../../../lib/layout-constants.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport { Badge } from \"../../ui/badge.js\";\nimport { Button } from \"../../ui/button.js\";\nimport { Card, CardContent } from \"../../ui/card.js\";\nimport { ScrollArea } from \"../../ui/scroll-area.js\";\nimport { Skeleton } from \"../../ui/skeleton.js\";\nimport { PostButton } from \"./PostButton.js\";\nimport { PostCard } from \"./PostCard.js\";\n\nexport interface SocialFeedProps {\n\tdata?: PostsResponse;\n\tloading?: boolean;\n\thasMore?: boolean;\n\tshowCreatePost?: boolean;\n\tshowFilters?: boolean;\n\tfeedType?: \"timeline\" | \"following\" | \"trending\" | \"user\" | \"contextual\";\n\tuserId?: string;\n\tcontext?: {\n\t\ttype: \"tx\" | \"url\" | \"channel\";\n\t\tvalue: string;\n\t\tlabel?: string; // e.g., \"Comments on 'Album Name'\"\n\t};\n\tonLoadMore?: () => void;\n\tonUserClick?: (identity: BapIdentity) => void;\n\tonRefresh?: () => void;\n\tonPost?: (content: string) => void; // For contextual posting\n\tclassName?: string;\n\tmaxHeight?: string | number;\n}\n\n/**\n * SocialFeed displays a chronological feed of social posts from the Bitcoin blockchain\n *\n * Features:\n * - Infinite scroll loading\n * - Post creation integration\n * - Multiple feed types (timeline, following, trending, user, contextual)\n * - Contextual comments (e.g., comments on albums, tracks, URLs)\n * - Real-time updates\n * - Pull-to-refresh\n * - Content filtering\n * - User interaction\n */\nexport function SocialFeed({\n\tdata,\n\tloading = false,\n\thasMore = false,\n\tshowCreatePost = true,\n\tshowFilters = false,\n\tfeedType = \"timeline\",\n\tuserId,\n\tcontext,\n\tonLoadMore,\n\tonUserClick,\n\tonRefresh,\n\tonPost,\n\tclassName = \"\",\n\tmaxHeight,\n}: SocialFeedProps) {\n\tconst [filter, setFilter] = useState<\"all\" | \"following\" | \"trending\">(\"all\");\n\tconst [refreshing, setRefreshing] = useState(false);\n\n\tconst handleRefresh = async () => {\n\t\tif (onRefresh && !refreshing) {\n\t\t\tsetRefreshing(true);\n\t\t\tawait onRefresh();\n\t\t\tsetRefreshing(false);\n\t\t}\n\t};\n\n\tconst getFeedTitle = () => {\n\t\tswitch (feedType) {\n\t\t\tcase \"timeline\":\n\t\t\t\treturn \"Timeline\";\n\t\t\tcase \"following\":\n\t\t\t\treturn \"Following\";\n\t\t\tcase \"trending\":\n\t\t\t\treturn \"Trending\";\n\t\t\tcase \"user\":\n\t\t\t\treturn userId ? \"User Posts\" : \"Posts\";\n\t\t\tcase \"contextual\":\n\t\t\t\treturn context?.label || \"Comments\";\n\t\t\tdefault:\n\t\t\t\treturn \"Social Feed\";\n\t\t}\n\t};\n\n\tconst posts = data?.results || [];\n\tconst signers = data?.signers || [];\n\tconst meta = data?.meta || [];\n\n\t// Create lookup maps for signers and meta\n\tconst signersMap = new Map<string, BapIdentity>();\n\tfor (const signer of signers) {\n\t\tif (signer.currentAddress) signersMap.set(signer.currentAddress, signer);\n\t\tif (signer.idKey) signersMap.set(signer.idKey, signer);\n\t}\n\n\tconst metaMap = new Map<string, (typeof meta)[0]>();\n\tfor (const m of meta) {\n\t\tmetaMap.set(m.tx, m);\n\t}\n\n\tconst filteredPosts = posts.filter((post) => {\n\t\tif (filter === \"following\") {\n\t\t\t// This would need following data from API - currently not available\n\t\t\treturn true; // Show all for now\n\t\t}\n\t\tif (filter === \"trending\") {\n\t\t\tconst postMeta = metaMap.get(post.tx.h);\n\t\t\treturn (postMeta?.likes || 0) > 5; // Basic trending logic\n\t\t}\n\t\treturn true;\n\t});\n\n\treturn (\n\t\t<div className={cn(\"flex flex-col gap-6 w-full\", className)}>\n\t\t\t{/* Feed Header */}\n\t\t\t<div className=\"flex justify-between items-center\">\n\t\t\t\t<h2 className=\"text-2xl font-bold\">{getFeedTitle()}</h2>\n\n\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t{onRefresh && (\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\tonClick={handleRefresh}\n\t\t\t\t\t\t\tdisabled={refreshing}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{refreshing ? (\n\t\t\t\t\t\t\t\t<div className=\"animate-spin rounded-full h-4 w-4 border-b-2 border-current\" />\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\t\twidth=\"16\"\n\t\t\t\t\t\t\t\t\theight=\"16\"\n\t\t\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<title>Refresh</title>\n\t\t\t\t\t\t\t\t\t<path\n\t\t\t\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\t\t\t\tstrokeWidth={2}\n\t\t\t\t\t\t\t\t\t\td=\"M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t)}\n\n\t\t\t\t\t<Badge variant=\"secondary\">{filteredPosts.length} posts</Badge>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t{/* Filters */}\n\t\t\t{showFilters && (\n\t\t\t\t<div className=\"flex gap-2 flex-wrap\">\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant={filter === \"all\" ? \"default\" : \"secondary\"}\n\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\tonClick={() => setFilter(\"all\")}\n\t\t\t\t\t>\n\t\t\t\t\t\tAll\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant={filter === \"following\" ? \"default\" : \"secondary\"}\n\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\tonClick={() => setFilter(\"following\")}\n\t\t\t\t\t>\n\t\t\t\t\t\tFollowing\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant={filter === \"trending\" ? \"default\" : \"secondary\"}\n\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\tonClick={() => setFilter(\"trending\")}\n\t\t\t\t\t>\n\t\t\t\t\t\tTrending\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t{/* Create Post */}\n\t\t\t{showCreatePost && (\n\t\t\t\t<PostButton\n\t\t\t\t\ttriggerText={\n\t\t\t\t\t\tfeedType === \"contextual\" && context?.label\n\t\t\t\t\t\t\t? \"Add a comment...\"\n\t\t\t\t\t\t\t: \"What's happening on Bitcoin?\"\n\t\t\t\t\t}\n\t\t\t\t\tplaceholder={\n\t\t\t\t\t\tfeedType === \"contextual\"\n\t\t\t\t\t\t\t? \"Write a comment...\"\n\t\t\t\t\t\t\t: \"Share your thoughts...\"\n\t\t\t\t\t}\n\t\t\t\t\tenableMarkdown\n\t\t\t\t\tonPost={(result) => {\n\t\t\t\t\t\t// TODO: PostButton needs to be updated to work with PostTransaction\n\t\t\t\t\t\tconsole.log(\"Post created:\", result);\n\t\t\t\t\t\tif (onPost) {\n\t\t\t\t\t\t\t// PostButton returns string or object with content\n\t\t\t\t\t\t\tonPost(typeof result === \"string\" ? result : \"\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t)}\n\n\t\t\t{/* Feed Content */}\n\t\t\t<ScrollArea style={{ maxHeight: maxHeight || \"70vh\" }}>\n\t\t\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t\t\t{filteredPosts.length === 0 && !loading ? (\n\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t<CardContent className=\"flex flex-col items-center gap-4 py-8\">\n\t\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\t\twidth=\"48\"\n\t\t\t\t\t\t\t\t\theight=\"48\"\n\t\t\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t\t\topacity=\"0.5\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<title>No posts found</title>\n\t\t\t\t\t\t\t\t\t<path\n\t\t\t\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\t\t\t\tstrokeWidth={2}\n\t\t\t\t\t\t\t\t\t\td=\"M7 8h10m0 0V6a2 2 0 00-2-2H9a2 2 0 00-2 2v2m10 0v10a2 2 0 01-2 2H9a2 2 0 01-2-2V8m0 0V6a2 2 0 012-2h6a2 2 0 012 2v2\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t\t<p className=\"text-base text-muted-foreground text-center\">\n\t\t\t\t\t\t\t\t\t{filter === \"following\"\n\t\t\t\t\t\t\t\t\t\t? \"No posts from people you follow yet.\"\n\t\t\t\t\t\t\t\t\t\t: filter === \"trending\"\n\t\t\t\t\t\t\t\t\t\t\t? \"No trending posts found.\"\n\t\t\t\t\t\t\t\t\t\t\t: feedType === \"contextual\"\n\t\t\t\t\t\t\t\t\t\t\t\t? \"No comments yet.\"\n\t\t\t\t\t\t\t\t\t\t\t\t: \"No posts to show.\"}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t{showCreatePost && (\n\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground text-center\">\n\t\t\t\t\t\t\t\t\t\t{feedType === \"contextual\"\n\t\t\t\t\t\t\t\t\t\t\t? \"Be the first to comment!\"\n\t\t\t\t\t\t\t\t\t\t\t: \"Be the first to share something on the Bitcoin blockchain!\"}\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t{filteredPosts.map((post) => {\n\t\t\t\t\t\t\t\tconst postMeta = metaMap.get(post.tx.h);\n\t\t\t\t\t\t\t\tconst signerAddress = post.AIP[0]?.address;\n\t\t\t\t\t\t\t\tconst signer = signerAddress\n\t\t\t\t\t\t\t\t\t? signersMap.get(signerAddress)\n\t\t\t\t\t\t\t\t\t: undefined;\n\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t<PostCard\n\t\t\t\t\t\t\t\t\t\tkey={post.tx.h}\n\t\t\t\t\t\t\t\t\t\tpost={post}\n\t\t\t\t\t\t\t\t\t\tmeta={postMeta}\n\t\t\t\t\t\t\t\t\t\tsigner={signer}\n\t\t\t\t\t\t\t\t\t\tonUserClick={onUserClick}\n\t\t\t\t\t\t\t\t\t\tonReply={(post) => {\n\t\t\t\t\t\t\t\t\t\t\t// Handle reply - could open reply dialog\n\t\t\t\t\t\t\t\t\t\t\tconsole.log(\"Reply to post:\", post.tx.h);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\tonShare={(post) => {\n\t\t\t\t\t\t\t\t\t\t\t// Handle share - could copy link or open share dialog\n\t\t\t\t\t\t\t\t\t\t\tnavigator.clipboard?.writeText(\n\t\t\t\t\t\t\t\t\t\t\t\t`https://whatsonchain.com/tx/${post.tx.h}`,\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t})}\n\n\t\t\t\t\t\t\t{/* Load More */}\n\t\t\t\t\t\t\t{hasMore && (\n\t\t\t\t\t\t\t\t<div className=\"flex justify-center mt-6\">\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t\t\t\tsize=\"default\"\n\t\t\t\t\t\t\t\t\t\tonClick={onLoadMore}\n\t\t\t\t\t\t\t\t\t\tdisabled={loading}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{loading ? (\n\t\t\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"animate-spin rounded-full h-4 w-4 border-b-2 border-current mr-2\" />\n\t\t\t\t\t\t\t\t\t\t\t\tLoading...\n\t\t\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\"Load More Posts\"\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t{!hasMore && filteredPosts.length > 0 && (\n\t\t\t\t\t\t\t\t<div className=\"flex justify-center mt-6\">\n\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\tYou've reached the end of the feed\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</>\n\t\t\t\t\t)}\n\n\t\t\t\t\t{/* Loading State */}\n\t\t\t\t\t{loading && filteredPosts.length === 0 && (\n\t\t\t\t\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t\t\t\t\t{[1, 2, 3].map((i) => (\n\t\t\t\t\t\t\t\t<Card key={i}>\n\t\t\t\t\t\t\t\t\t<CardContent className=\"p-4\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"h-10 w-10 rounded-full\" />\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex flex-col gap-1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"h-4 w-[120px]\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"h-3 w-[80px]\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"h-[60px] w-full\" />\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex gap-4\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"h-6 w-[60px]\" />\n\t\t\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"h-6 w-[60px]\" />\n\t\t\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"h-6 w-[60px]\" />\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t</ScrollArea>\n\t\t</div>\n\t);\n}\n", "target": "<%- config.aliases.components %>/socialfeed.tsx" } ] }