From d4363da79a5384de09f40ea45398795b74a85671 Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Tue, 5 Aug 2025 12:34:01 +0530 Subject: [PATCH] feat: add feed-UI --- src/App.tsx | 2 + src/components/CreateCellDialog.tsx | 22 ++- src/components/FeedSidebar.tsx | 219 ++++++++++++++++++++++++++++ src/components/Header.tsx | 44 +++++- src/components/PostCard.tsx | 117 +++++++++++++++ src/components/ui/wallet-wizard.tsx | 4 +- src/pages/Dashboard.tsx | 16 +- src/pages/FeedPage.tsx | 151 +++++++++++++++++++ 8 files changed, 552 insertions(+), 23 deletions(-) create mode 100644 src/components/FeedSidebar.tsx create mode 100644 src/components/PostCard.tsx create mode 100644 src/pages/FeedPage.tsx diff --git a/src/App.tsx b/src/App.tsx index f522a7e..27d7e22 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,6 +22,7 @@ import CellPage from "./pages/CellPage"; import PostPage from "./pages/PostPage"; import NotFound from "./pages/NotFound"; import Dashboard from "./pages/Dashboard"; +import Index from "./pages/Index"; import { appkitConfig } from "./lib/identity/wallets/appkit"; import { createAppKit } from "@reown/appkit"; import { WagmiProvider } from "wagmi"; @@ -45,6 +46,7 @@ const App = () => ( } /> + } /> } /> } /> } /> diff --git a/src/components/CreateCellDialog.tsx b/src/components/CreateCellDialog.tsx index f09cb9a..a61c741 100644 --- a/src/components/CreateCellDialog.tsx +++ b/src/components/CreateCellDialog.tsx @@ -37,11 +37,19 @@ const formSchema = z.object({ }), }); -export function CreateCellDialog() { +interface CreateCellDialogProps { + open?: boolean; + onOpenChange?: (open: boolean) => void; +} + +export function CreateCellDialog({ open: externalOpen, onOpenChange }: CreateCellDialogProps = {}) { const { createCell, isPostingCell } = useForum(); const { isAuthenticated } = useAuth(); const { toast } = useToast(); - const [open, setOpen] = React.useState(false); + const [internalOpen, setInternalOpen] = React.useState(false); + + const open = externalOpen ?? internalOpen; + const setOpen = onOpenChange ?? setInternalOpen; const form = useForm>({ resolver: zodResolver(formSchema), @@ -77,9 +85,11 @@ export function CreateCellDialog() { return ( - - - + {!onOpenChange && ( + + + + )} Create a New Cell @@ -151,3 +161,5 @@ export function CreateCellDialog() { ); } + +export default CreateCellDialog; diff --git a/src/components/FeedSidebar.tsx b/src/components/FeedSidebar.tsx new file mode 100644 index 0000000..4bd8f8d --- /dev/null +++ b/src/components/FeedSidebar.tsx @@ -0,0 +1,219 @@ +import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { Plus, TrendingUp, Users, Eye } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { useForum } from '@/contexts/useForum'; +import { useAuth } from '@/contexts/useAuth'; +import { CypherImage } from '@/components/ui/CypherImage'; +import {CreateCellDialog} from '@/components/CreateCellDialog'; + +const FeedSidebar: React.FC = () => { + const { cells, posts } = useForum(); + const { currentUser, verificationStatus } = useAuth(); + const [showCreateCell, setShowCreateCell] = useState(false); + + // Calculate trending cells based on recent post activity + const trendingCells = cells + .map(cell => { + const cellPosts = posts.filter(post => post.cellId === cell.id); + const recentPosts = cellPosts.filter(post => + Date.now() - post.timestamp < 24 * 60 * 60 * 1000 // Last 24 hours + ); + const totalScore = cellPosts.reduce((sum, post) => + sum + (post.upvotes.length - post.downvotes.length), 0 + ); + + return { + ...cell, + postCount: cellPosts.length, + recentPostCount: recentPosts.length, + totalScore, + activity: recentPosts.length + (totalScore * 0.1) // Simple activity score + }; + }) + .sort((a, b) => b.activity - a.activity) + .slice(0, 5); + + // User's verification status display + const getVerificationBadge = () => { + if (!currentUser) { + return Not Connected; + } + + // Ethereum wallet with ENS + if (currentUser.walletType === 'ethereum') { + if (currentUser.ensName && verificationStatus === 'verified-owner') { + return ✓ Owns ENS: {currentUser.ensName}; + } else { + return Read-only (No ENS detected); + } + } + + // Bitcoin wallet with Ordinal + if (currentUser.walletType === 'bitcoin') { + if (verificationStatus === 'verified-owner') { + return ✓ Owns Ordinal; + } else { + return Read-only (No Ordinal detected); + } + } + + // Fallback cases + switch (verificationStatus) { + case 'verified-none': + return Read Only; + case 'verifying': + return Verifying...; + default: + return Not Connected; + } + }; + + return ( +
+ {/* User Info Card */} + {currentUser && ( + + + Your Account + + +
+ {currentUser.address.slice(0, 8)}...{currentUser.address.slice(-6)} +
+ {getVerificationBadge()} +
+
+ )} + + {/* Create Cell */} + + + + {verificationStatus !== 'verified-owner' && ( +

+ {currentUser?.walletType === 'ethereum' + ? 'Own an ENS name to create cells' + : 'Own a Bitcoin Ordinal to create cells' + } +

+ )} +
+
+ + {/* Trending Cells */} + + + + + Trending Cells + + + + {trendingCells.length === 0 ? ( +

No cells yet

+ ) : ( + trendingCells.map((cell, index) => ( + +
+ + {index + 1} + + +
+
+ r/{cell.name} +
+
+ {cell.postCount} posts +
+
+
+ {cell.recentPostCount > 0 && ( + + {cell.recentPostCount} new + + )} + + )) + )} +
+
+ + {/* All Cells */} + + + + + All Cells + + + + {cells.length === 0 ? ( +

No cells created yet

+ ) : ( +
+ {cells.slice(0, 8).map(cell => ( + + r/{cell.name} + + ))} + {cells.length > 8 && ( + + View all cells → + + )} +
+ )} +
+
+ + {/* About */} + + +
+

OpChan v1.0

+

Decentralized forum on Bitcoin Ordinals

+
+ + Powered by Waku +
+
+
+
+ + {/* Create Cell Dialog */} + +
+ ); +}; + +export default FeedSidebar; \ No newline at end of file diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 8f361dd..bd5e63e 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import { useAuth } from '@/contexts/useAuth'; import { useForum } from '@/contexts/useForum'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; -import { LogOut, Terminal, Wifi, WifiOff, AlertTriangle, CheckCircle, Key, RefreshCw, CircleSlash } from 'lucide-react'; +import { LogOut, Terminal, Wifi, WifiOff, AlertTriangle, CheckCircle, Key, RefreshCw, CircleSlash, Home, Grid3X3, Plus } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { useToast } from '@/components/ui/use-toast'; import { useAppKitAccount, useDisconnect } from '@reown/appkit/react'; @@ -22,6 +22,7 @@ const Header = () => { isWalletAvailable } = useAuth(); const { isNetworkConnected, isRefreshing } = useForum(); + const location = useLocation(); const { toast } = useToast(); // Use AppKit hooks for multi-chain support const bitcoinAccount = useAppKitAccount({ namespace: "bip122" }); @@ -125,12 +126,39 @@ const Header = () => { <>
-
- - - OpChan - - +
+
+ + + OpChan + +
+ + {/* Navigation Tabs */} +
diff --git a/src/components/PostCard.tsx b/src/components/PostCard.tsx new file mode 100644 index 0000000..9c19122 --- /dev/null +++ b/src/components/PostCard.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { ArrowUp, ArrowDown, MessageSquare } from 'lucide-react'; +import { formatDistanceToNow } from 'date-fns'; +import { Post } from '@/types'; +import { useForum } from '@/contexts/useForum'; +import { useAuth } from '@/contexts/useAuth'; + +interface PostCardProps { + post: Post; + commentCount?: number; +} + +const PostCard: React.FC = ({ post, commentCount = 0 }) => { + const { getCellById, votePost, isVoting } = useForum(); + const { isAuthenticated, currentUser } = useAuth(); + + const cell = getCellById(post.cellId); + const cellName = cell?.name || 'unknown'; + + // Calculate vote score + const score = post.upvotes.length - post.downvotes.length; + + // Check user's vote status + const userUpvoted = currentUser ? post.upvotes.some(vote => vote.author === currentUser.address) : false; + const userDownvoted = currentUser ? post.downvotes.some(vote => vote.author === currentUser.address) : false; + + // Truncate content for preview + const contentPreview = post.content.length > 200 + ? post.content.substring(0, 200) + '...' + : post.content; + + const handleVote = async (e: React.MouseEvent, isUpvote: boolean) => { + e.preventDefault(); // Prevent navigation when clicking vote buttons + if (!isAuthenticated) return; + await votePost(post.id, isUpvote); + }; + + return ( +
+
+ {/* Voting column */} +
+ + + 0 ? 'text-cyber-accent' : + score < 0 ? 'text-blue-400' : + 'text-cyber-neutral' + }`}> + {score} + + + +
+ + {/* Content column */} +
+ + {/* Post metadata */} +
+ r/{cellName} + + Posted by u/{post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)} + + {formatDistanceToNow(new Date(post.timestamp), { addSuffix: true })} +
+ + {/* Post title */} +

+ {post.title} +

+ + {/* Post content preview */} +

+ {contentPreview} +

+ + {/* Post actions */} +
+
+ + {commentCount} comments +
+ + +
+ +
+
+
+ ); +}; + +export default PostCard; \ No newline at end of file diff --git a/src/components/ui/wallet-wizard.tsx b/src/components/ui/wallet-wizard.tsx index 68e8e92..25a553e 100644 --- a/src/components/ui/wallet-wizard.tsx +++ b/src/components/ui/wallet-wizard.tsx @@ -82,9 +82,9 @@ export function WalletWizard({ return "pending"; } - // Step 3: Key delegation - completed when delegation is valid + // Step 3: Key delegation - completed when delegation is valid AND authenticated if (step === 3) { - if (isDelegationValid()) return "completed"; + if (isAuthenticated && isDelegationValid()) return "completed"; if (currentStep === step) return "current"; return "pending"; } diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index d103c7a..3bb400b 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,15 +1,15 @@ import React from 'react'; -import Index from './Index'; -import ActivityFeed from '@/components/ActivityFeed'; +import Header from '@/components/Header'; +import FeedPage from './FeedPage'; const Dashboard: React.FC = () => { return ( -
-
- - -
-
+
+
+
+ +
+
); }; diff --git a/src/pages/FeedPage.tsx b/src/pages/FeedPage.tsx new file mode 100644 index 0000000..51fbd21 --- /dev/null +++ b/src/pages/FeedPage.tsx @@ -0,0 +1,151 @@ +import React, { useMemo } from 'react'; +import { RefreshCw, Plus } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Skeleton } from '@/components/ui/skeleton'; +import PostCard from '@/components/PostCard'; +import FeedSidebar from '@/components/FeedSidebar'; +import { useForum } from '@/contexts/useForum'; +import { useAuth } from '@/contexts/useAuth'; + +const FeedPage: React.FC = () => { + const { + posts, + comments, + isInitialLoading, + isRefreshing, + refreshData + } = useForum(); + const { verificationStatus } = useAuth(); + + // Combine posts from all cells and sort by timestamp (newest first) + const allPosts = useMemo(() => { + return [...posts] + .sort((a, b) => b.timestamp - a.timestamp) + .filter(post => !post.moderated); // Hide moderated posts from main feed + }, [posts]); + + // Calculate comment counts for each post + const getCommentCount = (postId: string) => { + return comments.filter(comment => comment.postId === postId && !comment.moderated).length; + }; + + // Loading skeleton + if (isInitialLoading) { + return ( +
+
+
+ {/* Main feed skeleton */} +
+
+ {[...Array(5)].map((_, i) => ( +
+
+
+ + + +
+
+ + + + + +
+
+
+ ))} +
+
+ + {/* Sidebar skeleton */} +
+ {[...Array(3)].map((_, i) => ( +
+ +
+ + + +
+
+ ))} +
+
+
+
+ ); + } + + return ( +
+
+ {/* Page Header */} +
+
+

+ Popular Posts +

+

+ Latest posts from all cells +

+
+
+ +
+
+ +
+ {/* Main Feed */} +
+ {/* Posts Feed */} +
+ {allPosts.length === 0 ? ( +
+
+

+ No posts yet +

+

+ Be the first to create a post in a cell! +

+ {verificationStatus !== 'verified-owner' && ( +

+ Connect your wallet and verify Ordinal ownership to start posting +

+ )} +
+
+ ) : ( + allPosts.map(post => ( + + )) + )} +
+
+ + {/* Sidebar */} +
+ +
+
+
+
+ ); +}; + +export default FeedPage; \ No newline at end of file