diff --git a/src/App.tsx b/src/App.tsx index 9185ed2..7cc4bd7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,10 +18,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { AuthProvider } from "@/contexts/AuthContext"; import { ForumProvider } from "@/contexts/ForumContext"; -import Index from "./pages/Index"; import CellPage from "./pages/CellPage"; import PostPage from "./pages/PostPage"; import NotFound from "./pages/NotFound"; +import Dashboard from "./pages/Dashboard"; // Create a client const queryClient = new QueryClient(); @@ -35,7 +35,7 @@ const App = () => ( - } /> + } /> } /> } /> } /> diff --git a/src/components/ActivityFeed.tsx b/src/components/ActivityFeed.tsx new file mode 100644 index 0000000..0fa206f --- /dev/null +++ b/src/components/ActivityFeed.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { useForum } from '@/contexts/ForumContext'; +import { Link } from 'react-router-dom'; +import { formatDistanceToNow } from 'date-fns'; +import { Skeleton } from '@/components/ui/skeleton'; +import { MessageSquareText, Newspaper } from 'lucide-react'; + +interface FeedItemBase { + id: string; + type: 'post' | 'comment'; + timestamp: number; + ownerAddress: string; + cellId?: string; + postId?: string; +} + +interface PostFeedItem extends FeedItemBase { + type: 'post'; + title: string; + cellId: string; + postId: string; + commentCount: number; + voteCount: number; +} + +interface CommentFeedItem extends FeedItemBase { + type: 'comment'; + content: string; + postId: string; + voteCount: number; +} + +type FeedItem = PostFeedItem | CommentFeedItem; + +const ActivityFeed: React.FC = () => { + const { posts, comments, cells, getCellById, isInitialLoading } = useForum(); + + const combinedFeed: FeedItem[] = [ + ...posts.map((post): PostFeedItem => ({ + id: post.id, + type: 'post', + timestamp: post.timestamp, + ownerAddress: post.authorAddress, + title: post.title, + cellId: post.cellId, + postId: post.id, + commentCount: 0, + voteCount: post.upvotes.length - post.downvotes.length, + })), + ...comments.map((comment): CommentFeedItem | null => { + const parentPost = posts.find(p => p.id === comment.postId); + if (!parentPost) return null; + return { + id: comment.id, + type: 'comment', + timestamp: comment.timestamp, + ownerAddress: comment.authorAddress, + content: comment.content, + postId: comment.postId, + cellId: parentPost.cellId, + voteCount: comment.upvotes.length - comment.downvotes.length, + }; + }) + .filter((item): item is CommentFeedItem => item !== null), + ].sort((a, b) => b.timestamp - a.timestamp); + + const renderFeedItem = (item: FeedItem) => { + const cell = item.cellId ? getCellById(item.cellId) : undefined; + const ownerShort = `${item.ownerAddress.slice(0, 5)}...${item.ownerAddress.slice(-4)}`; + const timeAgo = formatDistanceToNow(new Date(item.timestamp), { addSuffix: true }); + + const linkTarget = item.type === 'post' ? `/post/${item.postId}` : `/post/${item.postId}#comment-${item.id}`; + + return ( + +
+ {item.type === 'post' ? : } + + {item.type === 'post' ? item.title : `Comment on: ${posts.find(p => p.id === item.postId)?.title || 'post'}`} + + by + {ownerShort} + {cell && ( + <> + in + /{cell.name} + + )} + {timeAgo} +
+ {item.type === 'comment' && ( +

+ {item.content} +

+ )} + + ); + }; + + if (isInitialLoading) { + return ( +
+

Latest Activity

+ {[...Array(5)].map((_, i) => ( +
+ + +
+ ))} +
+ ); + } + + return ( +
+

Latest Activity

+ {combinedFeed.length === 0 ? ( +

No activity yet. Be the first to post!

+ ) : ( + combinedFeed.map(renderFeedItem) + )} +
+ ); +}; + +export default ActivityFeed; \ No newline at end of file diff --git a/src/components/CellList.tsx b/src/components/CellList.tsx index 3a32510..b7d13c3 100644 --- a/src/components/CellList.tsx +++ b/src/components/CellList.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { useForum } from '@/contexts/ForumContext'; -import { Skeleton } from '@/components/ui/skeleton'; -import { Layout, MessageSquare, RefreshCw } from 'lucide-react'; +import { Layout, MessageSquare, RefreshCw, Loader2 } from 'lucide-react'; import { CreateCellDialog } from './CreateCellDialog'; import { Button } from '@/components/ui/button'; import { CypherImage } from './ui/CypherImage'; @@ -12,22 +11,10 @@ const CellList = () => { if (isInitialLoading) { return ( -
-

Loading Cells...

-
- {[...Array(4)].map((_, i) => ( -
-
- -
- - - -
-
-
- ))} -
+
+ +

Loading Cells...

+

Connecting to the network and fetching data...

); } @@ -57,7 +44,7 @@ const CellList = () => {
-
+
{cells.length === 0 ? (
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index ad70212..6274507 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -4,8 +4,8 @@ import { useAuth } from '@/contexts/AuthContext'; import { useForum } from '@/contexts/ForumContext'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; -import { ShieldCheck, LogOut, Terminal, Wifi, WifiOff, Eye, MessageSquare, RefreshCw, Key } from 'lucide-react'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { ShieldCheck, LogOut, Terminal, Wifi, WifiOff, AlertTriangle, CheckCircle, Key, RefreshCw, CircleSlash } from 'lucide-react'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; const Header = () => { const { @@ -37,20 +37,18 @@ const Header = () => { await delegateKey(); }; - // Format delegation time remaining for display const formatDelegationTime = () => { if (!isDelegationValid()) return null; const timeRemaining = delegationTimeRemaining(); const hours = Math.floor(timeRemaining / (1000 * 60 * 60)); const minutes = Math.floor((timeRemaining % (1000 * 60 * 60)) / (1000 * 60)); - - return `${hours}h ${minutes}m`; + + return `${hours}h ${minutes}m`; }; const renderDelegationButton = () => { - // Only show delegation button for verified Ordinal owners - if (verificationStatus !== 'verified-owner') return null; + if (verificationStatus !== 'verified-owner') return null; const hasValidDelegation = isDelegationValid(); const timeRemaining = formatDelegationTime(); @@ -61,22 +59,20 @@ const Header = () => { - + {hasValidDelegation ? ( -

You have a delegated browser key active for {timeRemaining}. - You won't need to sign messages with your wallet for most actions.

+

Browser key active for ~{timeRemaining}. Wallet signatures not needed for most actions.

) : ( -

Delegate a browser key to avoid signing every action with your wallet. - Improves UX by reducing wallet popups for 24 hours.

+

Delegate a browser key for 24h to avoid constant wallet signing.

)}
@@ -86,15 +82,23 @@ const Header = () => { const renderAccessBadge = () => { if (verificationStatus === 'unverified') { return ( - + + + + + +

Action Required

+

Verify your Ordinal ownership to enable posting, commenting, and voting.

+
+
); } @@ -102,86 +106,54 @@ const Header = () => { return ( - Verifying... + [VERIFYING...] ); } if (verificationStatus === 'verified-none') { return ( -
- - - - - Read-Only Access - - - -

Wallet Verified - No Ordinals Found

-

Your wallet has been verified but does not contain any Ordinal Operators.

-

You can browse content but cannot post, comment, or vote.

-
-
- - - - - -

Verify again

-
-
-
+ + + + + [VERIFIED | READ-ONLY] + + + +

Wallet Verified - No Ordinals

+

No Ordinal Operators found. Read-only access granted.

+ +
+
); } + // Verified - Ordinal Owner if (verificationStatus === 'verified-owner') { return ( -
- - - - - Full Access - - - -

Ordinal Operators Verified!

-

You have full forum access with permission to post, comment, and vote.

-
-
- - - - - -

Verify again

-
-
-
+ + + + + [OWNER ✔] + + + +

Ordinal Owner Verified!

+

Full forum access granted.

+ +
+
); } @@ -189,44 +161,38 @@ const Header = () => { }; return ( -
-
+
+
OpChan - - PoC v0.1 - +
-
+
-
- - {isNetworkConnected ? ( - <> - - Connected - - ) : ( - <> - - Offline - - )} - -
+ + {isNetworkConnected ? ( + <> + + WAKU: Connected + + ) : ( + <> + + WAKU: Offline + + )} +
- -

{isNetworkConnected - ? "Connected to Waku network" - : "Not connected to Waku network. Some features may be unavailable."}

+ +

{isNetworkConnected ? "Waku network connection active." : "Waku network connection lost."}

{isRefreshing &&

Refreshing data...

}
@@ -236,25 +202,38 @@ const Header = () => { variant="outline" size="sm" onClick={handleConnect} + className="text-xs px-2 h-7" > Connect Wallet ) : ( - <> +
{renderAccessBadge()} {renderDelegationButton()} - - {currentUser.address.slice(0, 6)}...{currentUser.address.slice(-4)} - - - + + + + {currentUser.address.slice(0, 5)}...{currentUser.address.slice(-4)} + + + +

{currentUser.address}

+
+
+ + + + + Disconnect Wallet + +
)}
diff --git a/src/components/PostDetail.tsx b/src/components/PostDetail.tsx index 90081f1..5b10591 100644 --- a/src/components/PostDetail.tsx +++ b/src/components/PostDetail.tsx @@ -4,8 +4,7 @@ import { useForum } from '@/contexts/ForumContext'; import { useAuth } from '@/contexts/AuthContext'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; -import { Skeleton } from '@/components/ui/skeleton'; -import { ArrowLeft, ArrowUp, ArrowDown, Clock, MessageCircle, Send, RefreshCw, Eye } from 'lucide-react'; +import { ArrowLeft, ArrowUp, ArrowDown, Clock, MessageCircle, Send, RefreshCw, Eye, Loader2 } from 'lucide-react'; import { formatDistanceToNow } from 'date-fns'; import { Comment } from '@/types'; import { CypherImage } from './ui/CypherImage'; @@ -35,15 +34,9 @@ const PostDetail = () => { if (isInitialLoading) { return ( -
- - - - -
- - -
+
+ +

Loading Post...

); } @@ -116,42 +109,44 @@ const PostDetail = () => { Back to /{cell?.name || 'cell'}/ -
-
- - {post.upvotes.length - post.downvotes.length} - -
- -
-

{post.title}

-

{post.content}

-
- - - {formatDistanceToNow(post.timestamp, { addSuffix: true })} - - - - {postComments.length} {postComments.length === 1 ? 'comment' : 'comments'} - - - {post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)} - +
+
+
+ + {post.upvotes.length - post.downvotes.length} + +
+ +
+

{post.title}

+

{post.content}

+
+ + + {formatDistanceToNow(post.timestamp, { addSuffix: true })} + + + + {postComments.length} {postComments.length === 1 ? 'comment' : 'comments'} + + + {post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)} + +
@@ -165,7 +160,7 @@ const PostDetail = () => { placeholder="Add a comment..." value={newComment} onChange={(e) => setNewComment(e.target.value)} - className="flex-1 bg-cyber-muted/50 border-cyber-muted resize-none" + className="flex-1 bg-secondary/40 border-muted resize-none rounded-sm text-sm p-2" disabled={isPostingComment} />
) : verificationStatus === 'verified-none' ? ( -
-
- +
+
+

Read-Only Mode

-

+

Your wallet has been verified but does not contain any Ordinal Operators. You can browse threads but cannot comment or vote.

) : ( -
-

Connect wallet and verify Ordinal ownership to comment

+
+

Connect wallet and verify Ordinal ownership to comment

@@ -200,25 +195,25 @@ const PostDetail = () => {
{postComments.length === 0 ? ( -
+

No comments yet

) : ( postComments.map(comment => ( -
+
-
+
- {comment.upvotes.length - comment.downvotes.length} + {comment.upvotes.length - comment.downvotes.length}
-
-
-
+
+
+
- + {comment.authorAddress.slice(0, 6)}...{comment.authorAddress.slice(-4)}
- + {formatDistanceToNow(comment.timestamp, { addSuffix: true })}
diff --git a/src/index.css b/src/index.css index f434f1c..7c672fc 100644 --- a/src/index.css +++ b/src/index.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap'); @tailwind base; @tailwind components; @@ -6,26 +6,25 @@ @layer base { :root { - /* Cypherpunk theme */ - --background: 226 20% 14%; /* cyber-dark */ - --foreground: 0 0% 94%; + --background: 226 20% 12%; + --foreground: 0 0% 95%; - --card: 226 20% 14%; + --card: 226 20% 12%; --card-foreground: 0 0% 94%; --popover: 226 20% 18%; --popover-foreground: 0 0% 94%; - --primary: 195 82% 42%; /* cyber-accent */ + --primary: 195 82% 42%; --primary-foreground: 0 0% 98%; - --secondary: 226 20% 18%; /* slightly lighter than background */ + --secondary: 226 20% 18%; --secondary-foreground: 0 0% 94%; - --muted: 226 13% 27%; /* cyber-muted */ - --muted-foreground: 225 6% 57%; /* cyber-neutral */ + --muted: 226 13% 27%; + --muted-foreground: 225 6% 57%; - --accent: 195 82% 42%; /* cyber-accent */ + --accent: 195 82% 42%; --accent-foreground: 0 0% 98%; --destructive: 0 84% 60%; @@ -35,7 +34,7 @@ --input: 226 13% 27%; --ring: 195 82% 42%; - --radius: 0.25rem; /* smaller radius for shaper edges */ + --radius: 0.25rem; --sidebar-background: 226 20% 14%; --sidebar-foreground: 0 0% 94%; @@ -55,9 +54,9 @@ body { @apply bg-background text-foreground font-mono; + font-family: 'IBM Plex Mono', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } - /* Terminal-like appearance */ ::-webkit-scrollbar { width: 8px; } @@ -75,22 +74,18 @@ background: hsl(var(--muted-foreground)); } - /* Add a subtle text-shadow to primary elements */ .text-glow { text-shadow: 0 0 8px rgba(15, 160, 206, 0.5); } - /* Custom focus styles */ *:focus-visible { @apply outline-none ring-2 ring-primary/70 ring-offset-1 ring-offset-background; } - /* Monospace styling */ h1, h2, h3, h4, h5, h6, button, input, textarea { @apply font-mono; } - /* Button styles */ .btn { @apply inline-flex items-center justify-center rounded-sm px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none disabled:opacity-50; } @@ -102,15 +97,14 @@ } .board-card { - @apply border border-cyber-muted bg-cyber-dark hover:bg-cyber-muted/20 transition-colors rounded-sm p-4 mb-4; + @apply border border-cyber-muted bg-cyber-dark hover:bg-cyber-muted/20 transition-colors rounded-sm p-3 mb-3; } .comment-card { - @apply border-l-2 border-cyber-muted pl-4 py-2 my-3 hover:border-cyber-accent transition-colors; + @apply border-l-2 border-muted pl-3 py-2 my-2 hover:border-primary transition-colors; } } -/* Cyberpunk glow animation for CypherImage */ @keyframes cyber-flicker { 0%, 100% { opacity: 1; diff --git a/src/pages/CellPage.tsx b/src/pages/CellPage.tsx index 4880fb3..ade9de2 100644 --- a/src/pages/CellPage.tsx +++ b/src/pages/CellPage.tsx @@ -1,4 +1,3 @@ - import React from 'react'; import Header from '@/components/Header'; import PostList from '@/components/PostList'; @@ -7,7 +6,7 @@ const CellPage = () => { return (
-
+