feat: activity feed, improve UI/UX

This commit is contained in:
Danish Arora 2025-04-24 17:35:31 +05:30
parent 8d378b9dd5
commit 8859b85367
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
9 changed files with 342 additions and 243 deletions

View File

@ -18,10 +18,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { AuthProvider } from "@/contexts/AuthContext"; import { AuthProvider } from "@/contexts/AuthContext";
import { ForumProvider } from "@/contexts/ForumContext"; import { ForumProvider } from "@/contexts/ForumContext";
import Index from "./pages/Index";
import CellPage from "./pages/CellPage"; import CellPage from "./pages/CellPage";
import PostPage from "./pages/PostPage"; import PostPage from "./pages/PostPage";
import NotFound from "./pages/NotFound"; import NotFound from "./pages/NotFound";
import Dashboard from "./pages/Dashboard";
// Create a client // Create a client
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@ -35,7 +35,7 @@ const App = () => (
<Toaster /> <Toaster />
<Sonner /> <Sonner />
<Routes> <Routes>
<Route path="/" element={<Index />} /> <Route path="/" element={<Dashboard />} />
<Route path="/cell/:cellId" element={<CellPage />} /> <Route path="/cell/:cellId" element={<CellPage />} />
<Route path="/post/:postId" element={<PostPage />} /> <Route path="/post/:postId" element={<PostPage />} />
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />

View File

@ -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 (
<Link
to={linkTarget}
key={item.id}
className="block border border-muted hover:border-primary/50 hover:bg-secondary/30 rounded-sm p-3 mb-3 transition-colors duration-150"
>
<div className="flex items-center text-xs text-muted-foreground mb-1.5">
{item.type === 'post' ? <Newspaper className="w-3.5 h-3.5 mr-1.5 text-primary/80" /> : <MessageSquareText className="w-3.5 h-3.5 mr-1.5 text-accent/80" />}
<span className="font-medium text-foreground/90 mr-1">
{item.type === 'post' ? item.title : `Comment on: ${posts.find(p => p.id === item.postId)?.title || 'post'}`}
</span>
by
<span className="font-medium text-foreground/70 mx-1">{ownerShort}</span>
{cell && (
<>
in
<span className="font-medium text-foreground/70 ml-1">/{cell.name}</span>
</>
)}
<span className="ml-auto">{timeAgo}</span>
</div>
{item.type === 'comment' && (
<p className="text-sm text-foreground/80 pl-5 truncate">
{item.content}
</p>
)}
</Link>
);
};
if (isInitialLoading) {
return (
<div className="mb-6">
<h2 className="text-lg font-semibold mb-3 text-primary">Latest Activity</h2>
{[...Array(5)].map((_, i) => (
<div key={i} className="border border-muted rounded-sm p-3 mb-3">
<Skeleton className="h-4 w-3/4 mb-2" />
<Skeleton className="h-3 w-1/2" />
</div>
))}
</div>
);
}
return (
<div className="mb-6">
<h2 className="text-lg font-semibold mb-3 text-primary">Latest Activity</h2>
{combinedFeed.length === 0 ? (
<p className="text-muted-foreground text-sm">No activity yet. Be the first to post!</p>
) : (
combinedFeed.map(renderFeedItem)
)}
</div>
);
};
export default ActivityFeed;

View File

@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useForum } from '@/contexts/ForumContext'; import { useForum } from '@/contexts/ForumContext';
import { Skeleton } from '@/components/ui/skeleton'; import { Layout, MessageSquare, RefreshCw, Loader2 } from 'lucide-react';
import { Layout, MessageSquare, RefreshCw } from 'lucide-react';
import { CreateCellDialog } from './CreateCellDialog'; import { CreateCellDialog } from './CreateCellDialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { CypherImage } from './ui/CypherImage'; import { CypherImage } from './ui/CypherImage';
@ -12,22 +11,10 @@ const CellList = () => {
if (isInitialLoading) { if (isInitialLoading) {
return ( return (
<div className="container mx-auto px-4 py-8 max-w-4xl"> <div className="container mx-auto px-4 py-16 text-center">
<h1 className="text-2xl font-bold mb-6 text-glow">Loading Cells...</h1> <Loader2 className="w-8 h-8 mx-auto mb-4 animate-spin text-primary" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <p className="text-lg font-medium text-muted-foreground">Loading Cells...</p>
{[...Array(4)].map((_, i) => ( <p className="text-sm text-muted-foreground/70 mt-1">Connecting to the network and fetching data...</p>
<div key={i} className="border border-cyber-muted rounded-sm p-4">
<div className="flex gap-4 items-start">
<Skeleton className="w-16 h-16 rounded-sm bg-cyber-muted" />
<div className="flex-1">
<Skeleton className="h-6 w-24 mb-2 bg-cyber-muted" />
<Skeleton className="h-4 w-full mb-1 bg-cyber-muted" />
<Skeleton className="h-4 w-1/2 bg-cyber-muted" />
</div>
</div>
</div>
))}
</div>
</div> </div>
); );
} }
@ -57,7 +44,7 @@ const CellList = () => {
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{cells.length === 0 ? ( {cells.length === 0 ? (
<div className="col-span-2 text-center py-12"> <div className="col-span-2 text-center py-12">
<div className="text-cyber-neutral mb-4"> <div className="text-cyber-neutral mb-4">

View File

@ -4,8 +4,8 @@ import { useAuth } from '@/contexts/AuthContext';
import { useForum } from '@/contexts/ForumContext'; import { useForum } from '@/contexts/ForumContext';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { ShieldCheck, LogOut, Terminal, Wifi, WifiOff, Eye, MessageSquare, RefreshCw, Key } from 'lucide-react'; import { ShieldCheck, LogOut, Terminal, Wifi, WifiOff, AlertTriangle, CheckCircle, Key, RefreshCw, CircleSlash } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
const Header = () => { const Header = () => {
const { const {
@ -37,7 +37,6 @@ const Header = () => {
await delegateKey(); await delegateKey();
}; };
// Format delegation time remaining for display
const formatDelegationTime = () => { const formatDelegationTime = () => {
if (!isDelegationValid()) return null; if (!isDelegationValid()) return null;
@ -49,7 +48,6 @@ const Header = () => {
}; };
const renderDelegationButton = () => { 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 hasValidDelegation = isDelegationValid();
@ -61,22 +59,20 @@ const Header = () => {
<Button <Button
variant={hasValidDelegation ? "outline" : "default"} variant={hasValidDelegation ? "outline" : "default"}
size="sm" size="sm"
className="flex items-center gap-1" className="flex items-center gap-1 text-xs px-2 h-7"
onClick={handleDelegateKey} onClick={handleDelegateKey}
> >
<Key className="w-4 h-4" /> <Key className="w-3 h-3" />
{hasValidDelegation {hasValidDelegation
? <span>Key Delegated ({timeRemaining})</span> ? <span>KEY ACTIVE ({timeRemaining})</span>
: <span>Delegate Key</span>} : <span>DELEGATE KEY</span>}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="max-w-[260px]"> <TooltipContent className="max-w-[260px] text-sm">
{hasValidDelegation ? ( {hasValidDelegation ? (
<p>You have a delegated browser key active for {timeRemaining}. <p>Browser key active for ~{timeRemaining}. Wallet signatures not needed for most actions.</p>
You won't need to sign messages with your wallet for most actions.</p>
) : ( ) : (
<p>Delegate a browser key to avoid signing every action with your wallet. <p>Delegate a browser key for 24h to avoid constant wallet signing.</p>
Improves UX by reducing wallet popups for 24 hours.</p>
)} )}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -86,15 +82,23 @@ const Header = () => {
const renderAccessBadge = () => { const renderAccessBadge = () => {
if (verificationStatus === 'unverified') { if (verificationStatus === 'unverified') {
return ( return (
<Button <Tooltip>
variant="outline" <TooltipTrigger asChild>
size="sm" <Button
onClick={handleVerify} variant="outline"
className="flex items-center gap-1" size="sm"
> onClick={handleVerify}
<ShieldCheck className="w-4 h-4" /> className="flex items-center gap-1 text-xs px-2 h-7 border-destructive text-destructive hover:bg-destructive/10"
<span>Verify Ordinal</span> >
</Button> <AlertTriangle className="w-3 h-3" />
<span>[UNVERIFIED] Verify</span>
</Button>
</TooltipTrigger>
<TooltipContent className="max-w-[260px] text-sm">
<p className="font-semibold mb-1">Action Required</p>
<p>Verify your Ordinal ownership to enable posting, commenting, and voting.</p>
</TooltipContent>
</Tooltip>
); );
} }
@ -102,86 +106,54 @@ const Header = () => {
return ( return (
<Badge <Badge
variant="outline" variant="outline"
className="flex items-center gap-1" className="flex items-center gap-1 text-xs px-2 h-7"
> >
<RefreshCw className="w-3 h-3 animate-spin" /> <RefreshCw className="w-3 h-3 animate-spin" />
<span className="text-xs">Verifying...</span> <span>[VERIFYING...]</span>
</Badge> </Badge>
); );
} }
if (verificationStatus === 'verified-none') { if (verificationStatus === 'verified-none') {
return ( return (
<div className="flex items-center gap-2"> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Badge
<Badge variant="secondary"
variant="secondary" className="flex items-center gap-1 cursor-help text-xs px-2 h-7"
className="flex items-center gap-1 cursor-help" >
> <CircleSlash className="w-3 h-3" />
<Eye className="w-3 h-3" /> <span>[VERIFIED | READ-ONLY]</span>
<span className="text-xs">Read-Only Access</span> </Badge>
</Badge> </TooltipTrigger>
</TooltipTrigger> <TooltipContent className="max-w-[260px] text-sm">
<TooltipContent className="max-w-[260px]"> <p className="font-semibold mb-1">Wallet Verified - No Ordinals</p>
<p className="font-semibold mb-1">Wallet Verified - No Ordinals Found</p> <p>No Ordinal Operators found. Read-only access granted.</p>
<p className="text-sm mb-1">Your wallet has been verified but does not contain any Ordinal Operators.</p> <Button size="sm" variant="link" onClick={handleVerify} className="p-0 h-auto mt-1 text-xs">Verify Again?</Button>
<p className="text-sm text-muted-foreground">You can browse content but cannot post, comment, or vote.</p> </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={handleVerify}
>
<RefreshCw className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Verify again</p>
</TooltipContent>
</Tooltip>
</div>
); );
} }
// Verified - Ordinal Owner
if (verificationStatus === 'verified-owner') { if (verificationStatus === 'verified-owner') {
return ( return (
<div className="flex items-center gap-2"> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Badge
<Badge variant="default"
variant="default" className="flex items-center gap-1 cursor-help text-xs px-2 h-7 bg-primary text-primary-foreground"
className="flex items-center gap-1 cursor-help bg-cyber-accent" >
> <CheckCircle className="w-3 h-3" />
<MessageSquare className="w-3 h-3" /> <span>[OWNER ]</span>
<span className="text-xs">Full Access</span> </Badge>
</Badge> </TooltipTrigger>
</TooltipTrigger> <TooltipContent className="max-w-[260px] text-sm">
<TooltipContent className="max-w-[260px]"> <p className="font-semibold mb-1">Ordinal Owner Verified!</p>
<p className="font-semibold mb-1">Ordinal Operators Verified!</p> <p>Full forum access granted.</p>
<p className="text-sm">You have full forum access with permission to post, comment, and vote.</p> <Button size="sm" variant="link" onClick={handleVerify} className="p-0 h-auto mt-1 text-xs">Verify Again?</Button>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={handleVerify}
>
<RefreshCw className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Verify again</p>
</TooltipContent>
</Tooltip>
</div>
); );
} }
@ -189,44 +161,38 @@ const Header = () => {
}; };
return ( return (
<header className="border-b border-cyber-muted bg-cyber-dark"> <header className="border-b border-cyber-muted bg-cyber-dark fixed top-0 left-0 right-0 z-50 h-16">
<div className="container mx-auto px-4 py-4 flex justify-between items-center"> <div className="container mx-auto px-4 h-full flex justify-between items-center">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Terminal className="text-cyber-accent w-6 h-6" /> <Terminal className="text-cyber-accent w-6 h-6" />
<Link to="/" className="text-xl font-bold text-glow text-cyber-accent"> <Link to="/" className="text-xl font-bold text-glow text-cyber-accent">
OpChan OpChan
</Link> </Link>
<span className="text-xs bg-cyber-muted px-2 py-0.5 rounded ml-2">
PoC v0.1
</span>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-3 items-center">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className="flex items-center"> <Badge
<Badge variant={isNetworkConnected ? "default" : "destructive"}
variant={isNetworkConnected ? "default" : "destructive"} className="flex items-center gap-1 text-xs px-2 h-7 cursor-help"
className="flex items-center gap-1 mr-2" >
> {isNetworkConnected ? (
{isNetworkConnected ? ( <>
<> <Wifi className="w-3 h-3" />
<Wifi className="w-3 h-3" /> <span>WAKU: Connected</span>
<span className="text-xs">Connected</span> </>
</> ) : (
) : ( <>
<> <WifiOff className="w-3 h-3" />
<WifiOff className="w-3 h-3" /> <span>WAKU: Offline</span>
<span className="text-xs">Offline</span> </>
</> )}
)} </Badge>
</Badge>
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent className="text-sm">
<p>{isNetworkConnected <p>{isNetworkConnected ? "Waku network connection active." : "Waku network connection lost."}</p>
? "Connected to Waku network"
: "Not connected to Waku network. Some features may be unavailable."}</p>
{isRefreshing && <p>Refreshing data...</p>} {isRefreshing && <p>Refreshing data...</p>}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -236,25 +202,38 @@ const Header = () => {
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleConnect} onClick={handleConnect}
className="text-xs px-2 h-7"
> >
Connect Wallet Connect Wallet
</Button> </Button>
) : ( ) : (
<> <div className="flex gap-2 items-center">
{renderAccessBadge()} {renderAccessBadge()}
{renderDelegationButton()} {renderDelegationButton()}
<span className="hidden md:flex items-center text-sm text-cyber-neutral px-3"> <Tooltip>
{currentUser.address.slice(0, 6)}...{currentUser.address.slice(-4)} <TooltipTrigger asChild>
</span> <span className="hidden md:flex items-center text-xs text-muted-foreground cursor-default px-2 h-7">
<Button {currentUser.address.slice(0, 5)}...{currentUser.address.slice(-4)}
variant="ghost" </span>
size="icon" </TooltipTrigger>
onClick={handleDisconnect} <TooltipContent className="text-sm">
title="Disconnect wallet" <p>{currentUser.address}</p>
> </TooltipContent>
<LogOut className="w-4 h-4" /> </Tooltip>
</Button> <Tooltip>
</> <TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={handleDisconnect}
className="w-7 h-7"
>
<LogOut className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent className="text-sm">Disconnect Wallet</TooltipContent>
</Tooltip>
</div>
)} )}
</div> </div>
</div> </div>

View File

@ -4,8 +4,7 @@ import { useForum } from '@/contexts/ForumContext';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Skeleton } from '@/components/ui/skeleton'; import { ArrowLeft, ArrowUp, ArrowDown, Clock, MessageCircle, Send, RefreshCw, Eye, Loader2 } from 'lucide-react';
import { ArrowLeft, ArrowUp, ArrowDown, Clock, MessageCircle, Send, RefreshCw, Eye } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { Comment } from '@/types'; import { Comment } from '@/types';
import { CypherImage } from './ui/CypherImage'; import { CypherImage } from './ui/CypherImage';
@ -35,15 +34,9 @@ const PostDetail = () => {
if (isInitialLoading) { if (isInitialLoading) {
return ( return (
<div className="container mx-auto px-4 py-6"> <div className="container mx-auto px-4 py-16 text-center">
<Skeleton className="h-6 w-32 mb-6" /> <Loader2 className="w-8 h-8 mx-auto mb-4 animate-spin text-primary" />
<Skeleton className="h-10 w-3/4 mb-3" /> <p className="text-lg font-medium text-muted-foreground">Loading Post...</p>
<Skeleton className="h-32 w-full mb-6" />
<Skeleton className="h-6 w-48 mb-4" />
<div className="space-y-4">
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
</div>
</div> </div>
); );
} }
@ -116,42 +109,44 @@ const PostDetail = () => {
Back to /{cell?.name || 'cell'}/ Back to /{cell?.name || 'cell'}/
</Button> </Button>
<div className="flex gap-4 items-start"> <div className="border border-muted rounded-sm p-3 mb-6">
<div className="flex flex-col items-center"> <div className="flex gap-3 items-start">
<button <div className="flex flex-col items-center w-6 pt-1">
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${isPostUpvoted ? 'text-cyber-accent' : ''}`} <button
onClick={() => handleVotePost(true)} className={`p-1 rounded-sm hover:bg-secondary/50 disabled:opacity-50 ${isPostUpvoted ? 'text-primary' : ''}`}
disabled={verificationStatus !== 'verified-owner' || isVoting} onClick={() => handleVotePost(true)}
title={verificationStatus === 'verified-owner' ? "Upvote" : "Full access required to vote"} disabled={verificationStatus !== 'verified-owner' || isVoting}
> title={verificationStatus === 'verified-owner' ? "Upvote" : "Full access required to vote"}
<ArrowUp className="w-5 h-5" /> >
</button> <ArrowUp className="w-5 h-5" />
<span className="text-sm py-1">{post.upvotes.length - post.downvotes.length}</span> </button>
<button <span className="text-sm font-medium py-1">{post.upvotes.length - post.downvotes.length}</span>
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${isPostDownvoted ? 'text-cyber-accent' : ''}`} <button
onClick={() => handleVotePost(false)} className={`p-1 rounded-sm hover:bg-secondary/50 disabled:opacity-50 ${isPostDownvoted ? 'text-primary' : ''}`}
disabled={verificationStatus !== 'verified-owner' || isVoting} onClick={() => handleVotePost(false)}
title={verificationStatus === 'verified-owner' ? "Downvote" : "Full access required to vote"} disabled={verificationStatus !== 'verified-owner' || isVoting}
> title={verificationStatus === 'verified-owner' ? "Downvote" : "Full access required to vote"}
<ArrowDown className="w-5 h-5" /> >
</button> <ArrowDown className="w-5 h-5" />
</div> </button>
</div>
<div className="flex-1"> <div className="flex-1">
<h2 className="text-xl font-bold mb-2">{post.title}</h2> <h2 className="text-xl font-bold mb-2 text-foreground">{post.title}</h2>
<p className="text-lg mb-4">{post.content}</p> <p className="text-base mb-4 text-foreground/90">{post.content}</p>
<div className="flex items-center gap-4 text-xs text-cyber-neutral"> <div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center"> <span className="flex items-center">
<Clock className="w-3 h-3 mr-1" /> <Clock className="w-3 h-3 mr-1" />
{formatDistanceToNow(post.timestamp, { addSuffix: true })} {formatDistanceToNow(post.timestamp, { addSuffix: true })}
</span> </span>
<span className="flex items-center"> <span className="flex items-center">
<MessageCircle className="w-3 h-3 mr-1" /> <MessageCircle className="w-3 h-3 mr-1" />
{postComments.length} {postComments.length === 1 ? 'comment' : 'comments'} {postComments.length} {postComments.length === 1 ? 'comment' : 'comments'}
</span> </span>
<span className="truncate max-w-[150px]"> <span className="truncate max-w-[150px]">
{post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)} {post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)}
</span> </span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -165,7 +160,7 @@ const PostDetail = () => {
placeholder="Add a comment..." placeholder="Add a comment..."
value={newComment} value={newComment}
onChange={(e) => setNewComment(e.target.value)} 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} disabled={isPostingComment}
/> />
<Button <Button
@ -179,19 +174,19 @@ const PostDetail = () => {
</form> </form>
</div> </div>
) : verificationStatus === 'verified-none' ? ( ) : verificationStatus === 'verified-none' ? (
<div className="mb-8 p-4 border border-cyber-muted rounded-sm bg-cyber-muted/20"> <div className="mb-8 p-3 border border-muted rounded-sm bg-secondary/30">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-1.5">
<Eye className="w-4 h-4 text-cyber-neutral" /> <Eye className="w-4 h-4 text-muted-foreground" />
<h3 className="font-medium">Read-Only Mode</h3> <h3 className="font-medium">Read-Only Mode</h3>
</div> </div>
<p className="text-sm text-cyber-neutral"> <p className="text-sm text-muted-foreground">
Your wallet has been verified but does not contain any Ordinal Operators. Your wallet has been verified but does not contain any Ordinal Operators.
You can browse threads but cannot comment or vote. You can browse threads but cannot comment or vote.
</p> </p>
</div> </div>
) : ( ) : (
<div className="mb-8 p-4 border border-cyber-muted rounded-sm bg-cyber-muted/20 text-center"> <div className="mb-8 p-3 border border-muted rounded-sm bg-secondary/30 text-center">
<p className="text-sm mb-3">Connect wallet and verify Ordinal ownership to comment</p> <p className="text-sm mb-2">Connect wallet and verify Ordinal ownership to comment</p>
<Button asChild size="sm"> <Button asChild size="sm">
<Link to="/">Go to Home</Link> <Link to="/">Go to Home</Link>
</Button> </Button>
@ -200,25 +195,25 @@ const PostDetail = () => {
<div className="space-y-2"> <div className="space-y-2">
{postComments.length === 0 ? ( {postComments.length === 0 ? (
<div className="text-center py-6 text-cyber-neutral"> <div className="text-center py-6 text-muted-foreground">
<p>No comments yet</p> <p>No comments yet</p>
</div> </div>
) : ( ) : (
postComments.map(comment => ( postComments.map(comment => (
<div key={comment.id} className="comment-card"> <div key={comment.id} className="comment-card" id={`comment-${comment.id}`}>
<div className="flex gap-2 items-start"> <div className="flex gap-2 items-start">
<div className="flex flex-col items-center mr-2"> <div className="flex flex-col items-center w-5 pt-0.5">
<button <button
className={`p-0.5 rounded-sm hover:bg-cyber-muted/50 ${isCommentVoted(comment, true) ? 'text-cyber-accent' : ''}`} className={`p-0.5 rounded-sm hover:bg-secondary/50 disabled:opacity-50 ${isCommentVoted(comment, true) ? 'text-primary' : ''}`}
onClick={() => handleVoteComment(comment.id, true)} onClick={() => handleVoteComment(comment.id, true)}
disabled={verificationStatus !== 'verified-owner' || isVoting} disabled={verificationStatus !== 'verified-owner' || isVoting}
title={verificationStatus === 'verified-owner' ? "Upvote" : "Full access required to vote"} title={verificationStatus === 'verified-owner' ? "Upvote" : "Full access required to vote"}
> >
<ArrowUp className="w-4 h-4" /> <ArrowUp className="w-4 h-4" />
</button> </button>
<span className="text-xs py-0.5">{comment.upvotes.length - comment.downvotes.length}</span> <span className="text-xs font-medium py-0.5">{comment.upvotes.length - comment.downvotes.length}</span>
<button <button
className={`p-0.5 rounded-sm hover:bg-cyber-muted/50 ${isCommentVoted(comment, false) ? 'text-cyber-accent' : ''}`} className={`p-0.5 rounded-sm hover:bg-secondary/50 disabled:opacity-50 ${isCommentVoted(comment, false) ? 'text-primary' : ''}`}
onClick={() => handleVoteComment(comment.id, false)} onClick={() => handleVoteComment(comment.id, false)}
disabled={verificationStatus !== 'verified-owner' || isVoting} disabled={verificationStatus !== 'verified-owner' || isVoting}
title={verificationStatus === 'verified-owner' ? "Downvote" : "Full access required to vote"} title={verificationStatus === 'verified-owner' ? "Downvote" : "Full access required to vote"}
@ -226,19 +221,19 @@ const PostDetail = () => {
<ArrowDown className="w-4 h-4" /> <ArrowDown className="w-4 h-4" />
</button> </button>
</div> </div>
<div className="bg-cyber-muted/30 rounded-sm p-3 flex-1"> <div className="flex-1 pt-0.5">
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-center mb-1.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-1.5">
<CypherImage <CypherImage
src={getIdentityImageUrl(comment.authorAddress)} src={getIdentityImageUrl(comment.authorAddress)}
alt={comment.authorAddress.slice(0, 6)} alt={comment.authorAddress.slice(0, 6)}
className="rounded-sm w-5 h-5 bg-cyber-muted" className="rounded-sm w-5 h-5 bg-secondary"
/> />
<span className="text-xs text-cyber-neutral"> <span className="text-xs text-muted-foreground">
{comment.authorAddress.slice(0, 6)}...{comment.authorAddress.slice(-4)} {comment.authorAddress.slice(0, 6)}...{comment.authorAddress.slice(-4)}
</span> </span>
</div> </div>
<span className="text-xs text-cyber-neutral"> <span className="text-xs text-muted-foreground">
{formatDistanceToNow(comment.timestamp, { addSuffix: true })} {formatDistanceToNow(comment.timestamp, { addSuffix: true })}
</span> </span>
</div> </div>

View File

@ -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 base;
@tailwind components; @tailwind components;
@ -6,26 +6,25 @@
@layer base { @layer base {
:root { :root {
/* Cypherpunk theme */ --background: 226 20% 12%;
--background: 226 20% 14%; /* cyber-dark */ --foreground: 0 0% 95%;
--foreground: 0 0% 94%;
--card: 226 20% 14%; --card: 226 20% 12%;
--card-foreground: 0 0% 94%; --card-foreground: 0 0% 94%;
--popover: 226 20% 18%; --popover: 226 20% 18%;
--popover-foreground: 0 0% 94%; --popover-foreground: 0 0% 94%;
--primary: 195 82% 42%; /* cyber-accent */ --primary: 195 82% 42%;
--primary-foreground: 0 0% 98%; --primary-foreground: 0 0% 98%;
--secondary: 226 20% 18%; /* slightly lighter than background */ --secondary: 226 20% 18%;
--secondary-foreground: 0 0% 94%; --secondary-foreground: 0 0% 94%;
--muted: 226 13% 27%; /* cyber-muted */ --muted: 226 13% 27%;
--muted-foreground: 225 6% 57%; /* cyber-neutral */ --muted-foreground: 225 6% 57%;
--accent: 195 82% 42%; /* cyber-accent */ --accent: 195 82% 42%;
--accent-foreground: 0 0% 98%; --accent-foreground: 0 0% 98%;
--destructive: 0 84% 60%; --destructive: 0 84% 60%;
@ -35,7 +34,7 @@
--input: 226 13% 27%; --input: 226 13% 27%;
--ring: 195 82% 42%; --ring: 195 82% 42%;
--radius: 0.25rem; /* smaller radius for shaper edges */ --radius: 0.25rem;
--sidebar-background: 226 20% 14%; --sidebar-background: 226 20% 14%;
--sidebar-foreground: 0 0% 94%; --sidebar-foreground: 0 0% 94%;
@ -55,9 +54,9 @@
body { body {
@apply bg-background text-foreground font-mono; @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 { ::-webkit-scrollbar {
width: 8px; width: 8px;
} }
@ -75,22 +74,18 @@
background: hsl(var(--muted-foreground)); background: hsl(var(--muted-foreground));
} }
/* Add a subtle text-shadow to primary elements */
.text-glow { .text-glow {
text-shadow: 0 0 8px rgba(15, 160, 206, 0.5); text-shadow: 0 0 8px rgba(15, 160, 206, 0.5);
} }
/* Custom focus styles */
*:focus-visible { *:focus-visible {
@apply outline-none ring-2 ring-primary/70 ring-offset-1 ring-offset-background; @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 { h1, h2, h3, h4, h5, h6, button, input, textarea {
@apply font-mono; @apply font-mono;
} }
/* Button styles */
.btn { .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; @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 { .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 { .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 { @keyframes cyber-flicker {
0%, 100% { 0%, 100% {
opacity: 1; opacity: 1;

View File

@ -1,4 +1,3 @@
import React from 'react'; import React from 'react';
import Header from '@/components/Header'; import Header from '@/components/Header';
import PostList from '@/components/PostList'; import PostList from '@/components/PostList';
@ -7,7 +6,7 @@ const CellPage = () => {
return ( return (
<div className="min-h-screen flex flex-col bg-cyber-dark text-white"> <div className="min-h-screen flex flex-col bg-cyber-dark text-white">
<Header /> <Header />
<main className="flex-1"> <main className="flex-1 pt-16">
<PostList /> <PostList />
</main> </main>
<footer className="border-t border-cyber-muted py-4 text-center text-xs text-cyber-neutral"> <footer className="border-t border-cyber-muted py-4 text-center text-xs text-cyber-neutral">

16
src/pages/Dashboard.tsx Normal file
View File

@ -0,0 +1,16 @@
import React from 'react';
import Index from './Index';
import ActivityFeed from '@/components/ActivityFeed';
const Dashboard: React.FC = () => {
return (
<div className="pt-16">
<div className="container mx-auto px-4 py-6">
<ActivityFeed />
<Index />
</div>
</div>
);
};
export default Dashboard;

View File

@ -1,4 +1,3 @@
import React from 'react'; import React from 'react';
import Header from '@/components/Header'; import Header from '@/components/Header';
import PostDetail from '@/components/PostDetail'; import PostDetail from '@/components/PostDetail';
@ -7,7 +6,7 @@ const PostPage = () => {
return ( return (
<div className="min-h-screen flex flex-col bg-cyber-dark text-white"> <div className="min-h-screen flex flex-col bg-cyber-dark text-white">
<Header /> <Header />
<main className="flex-1"> <main className="flex-1 pt-16">
<PostDetail /> <PostDetail />
</main> </main>
<footer className="border-t border-cyber-muted py-4 text-center text-xs text-cyber-neutral"> <footer className="border-t border-cyber-muted py-4 text-center text-xs text-cyber-neutral">