mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 12:53:10 +00:00
feat: activity feed, improve UI/UX
This commit is contained in:
parent
8d378b9dd5
commit
8859b85367
@ -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 = () => (
|
||||
<Toaster />
|
||||
<Sonner />
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/cell/:cellId" element={<CellPage />} />
|
||||
<Route path="/post/:postId" element={<PostPage />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
|
||||
130
src/components/ActivityFeed.tsx
Normal file
130
src/components/ActivityFeed.tsx
Normal 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;
|
||||
@ -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 (
|
||||
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<h1 className="text-2xl font-bold mb-6 text-glow">Loading Cells...</h1>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<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 className="container mx-auto px-4 py-16 text-center">
|
||||
<Loader2 className="w-8 h-8 mx-auto mb-4 animate-spin text-primary" />
|
||||
<p className="text-lg font-medium text-muted-foreground">Loading Cells...</p>
|
||||
<p className="text-sm text-muted-foreground/70 mt-1">Connecting to the network and fetching data...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -57,7 +44,7 @@ const CellList = () => {
|
||||
</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 ? (
|
||||
<div className="col-span-2 text-center py-12">
|
||||
<div className="text-cyber-neutral mb-4">
|
||||
|
||||
@ -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 = () => {
|
||||
<Button
|
||||
variant={hasValidDelegation ? "outline" : "default"}
|
||||
size="sm"
|
||||
className="flex items-center gap-1"
|
||||
className="flex items-center gap-1 text-xs px-2 h-7"
|
||||
onClick={handleDelegateKey}
|
||||
>
|
||||
<Key className="w-4 h-4" />
|
||||
<Key className="w-3 h-3" />
|
||||
{hasValidDelegation
|
||||
? <span>Key Delegated ({timeRemaining})</span>
|
||||
: <span>Delegate Key</span>}
|
||||
? <span>KEY ACTIVE ({timeRemaining})</span>
|
||||
: <span>DELEGATE KEY</span>}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[260px]">
|
||||
<TooltipContent className="max-w-[260px] text-sm">
|
||||
{hasValidDelegation ? (
|
||||
<p>You have a delegated browser key active for {timeRemaining}.
|
||||
You won't need to sign messages with your wallet for most actions.</p>
|
||||
<p>Browser key active for ~{timeRemaining}. Wallet signatures not needed for most actions.</p>
|
||||
) : (
|
||||
<p>Delegate a browser key to avoid signing every action with your wallet.
|
||||
Improves UX by reducing wallet popups for 24 hours.</p>
|
||||
<p>Delegate a browser key for 24h to avoid constant wallet signing.</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@ -86,15 +82,23 @@ const Header = () => {
|
||||
const renderAccessBadge = () => {
|
||||
if (verificationStatus === 'unverified') {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleVerify}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
<span>Verify Ordinal</span>
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleVerify}
|
||||
className="flex items-center gap-1 text-xs px-2 h-7 border-destructive text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<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 (
|
||||
<Badge
|
||||
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" />
|
||||
<span className="text-xs">Verifying...</span>
|
||||
<span>[VERIFYING...]</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (verificationStatus === 'verified-none') {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1 cursor-help"
|
||||
>
|
||||
<Eye className="w-3 h-3" />
|
||||
<span className="text-xs">Read-Only Access</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[260px]">
|
||||
<p className="font-semibold mb-1">Wallet Verified - No Ordinals Found</p>
|
||||
<p className="text-sm mb-1">Your wallet has been verified but does not contain any Ordinal Operators.</p>
|
||||
<p className="text-sm text-muted-foreground">You can browse content but cannot post, comment, or vote.</p>
|
||||
</TooltipContent>
|
||||
</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>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1 cursor-help text-xs px-2 h-7"
|
||||
>
|
||||
<CircleSlash className="w-3 h-3" />
|
||||
<span>[VERIFIED | READ-ONLY]</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[260px] text-sm">
|
||||
<p className="font-semibold mb-1">Wallet Verified - No Ordinals</p>
|
||||
<p>No Ordinal Operators found. Read-only access granted.</p>
|
||||
<Button size="sm" variant="link" onClick={handleVerify} className="p-0 h-auto mt-1 text-xs">Verify Again?</Button>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// Verified - Ordinal Owner
|
||||
if (verificationStatus === 'verified-owner') {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="default"
|
||||
className="flex items-center gap-1 cursor-help bg-cyber-accent"
|
||||
>
|
||||
<MessageSquare className="w-3 h-3" />
|
||||
<span className="text-xs">Full Access</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[260px]">
|
||||
<p className="font-semibold mb-1">Ordinal Operators Verified!</p>
|
||||
<p className="text-sm">You have full forum access with permission to post, comment, and vote.</p>
|
||||
</TooltipContent>
|
||||
</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>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="default"
|
||||
className="flex items-center gap-1 cursor-help text-xs px-2 h-7 bg-primary text-primary-foreground"
|
||||
>
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
<span>[OWNER ✔]</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[260px] text-sm">
|
||||
<p className="font-semibold mb-1">Ordinal Owner Verified!</p>
|
||||
<p>Full forum access granted.</p>
|
||||
<Button size="sm" variant="link" onClick={handleVerify} className="p-0 h-auto mt-1 text-xs">Verify Again?</Button>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@ -189,44 +161,38 @@ const Header = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="border-b border-cyber-muted bg-cyber-dark">
|
||||
<div className="container mx-auto px-4 py-4 flex justify-between items-center">
|
||||
<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 h-full flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="text-cyber-accent w-6 h-6" />
|
||||
<Link to="/" className="text-xl font-bold text-glow text-cyber-accent">
|
||||
OpChan
|
||||
</Link>
|
||||
<span className="text-xs bg-cyber-muted px-2 py-0.5 rounded ml-2">
|
||||
PoC v0.1
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex gap-3 items-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Badge
|
||||
variant={isNetworkConnected ? "default" : "destructive"}
|
||||
className="flex items-center gap-1 mr-2"
|
||||
>
|
||||
{isNetworkConnected ? (
|
||||
<>
|
||||
<Wifi className="w-3 h-3" />
|
||||
<span className="text-xs">Connected</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<WifiOff className="w-3 h-3" />
|
||||
<span className="text-xs">Offline</span>
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge
|
||||
variant={isNetworkConnected ? "default" : "destructive"}
|
||||
className="flex items-center gap-1 text-xs px-2 h-7 cursor-help"
|
||||
>
|
||||
{isNetworkConnected ? (
|
||||
<>
|
||||
<Wifi className="w-3 h-3" />
|
||||
<span>WAKU: Connected</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<WifiOff className="w-3 h-3" />
|
||||
<span>WAKU: Offline</span>
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{isNetworkConnected
|
||||
? "Connected to Waku network"
|
||||
: "Not connected to Waku network. Some features may be unavailable."}</p>
|
||||
<TooltipContent className="text-sm">
|
||||
<p>{isNetworkConnected ? "Waku network connection active." : "Waku network connection lost."}</p>
|
||||
{isRefreshing && <p>Refreshing data...</p>}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@ -236,25 +202,38 @@ const Header = () => {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleConnect}
|
||||
className="text-xs px-2 h-7"
|
||||
>
|
||||
Connect Wallet
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex gap-2 items-center">
|
||||
{renderAccessBadge()}
|
||||
{renderDelegationButton()}
|
||||
<span className="hidden md:flex items-center text-sm text-cyber-neutral px-3">
|
||||
{currentUser.address.slice(0, 6)}...{currentUser.address.slice(-4)}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleDisconnect}
|
||||
title="Disconnect wallet"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="hidden md:flex items-center text-xs text-muted-foreground cursor-default px-2 h-7">
|
||||
{currentUser.address.slice(0, 5)}...{currentUser.address.slice(-4)}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-sm">
|
||||
<p>{currentUser.address}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<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>
|
||||
|
||||
@ -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 (
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<Skeleton className="h-6 w-32 mb-6" />
|
||||
<Skeleton className="h-10 w-3/4 mb-3" />
|
||||
<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 className="container mx-auto px-4 py-16 text-center">
|
||||
<Loader2 className="w-8 h-8 mx-auto mb-4 animate-spin text-primary" />
|
||||
<p className="text-lg font-medium text-muted-foreground">Loading Post...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -116,42 +109,44 @@ const PostDetail = () => {
|
||||
Back to /{cell?.name || 'cell'}/
|
||||
</Button>
|
||||
|
||||
<div className="flex gap-4 items-start">
|
||||
<div className="flex flex-col items-center">
|
||||
<button
|
||||
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${isPostUpvoted ? 'text-cyber-accent' : ''}`}
|
||||
onClick={() => handleVotePost(true)}
|
||||
disabled={verificationStatus !== 'verified-owner' || isVoting}
|
||||
title={verificationStatus === 'verified-owner' ? "Upvote" : "Full access required to vote"}
|
||||
>
|
||||
<ArrowUp className="w-5 h-5" />
|
||||
</button>
|
||||
<span className="text-sm py-1">{post.upvotes.length - post.downvotes.length}</span>
|
||||
<button
|
||||
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${isPostDownvoted ? 'text-cyber-accent' : ''}`}
|
||||
onClick={() => handleVotePost(false)}
|
||||
disabled={verificationStatus !== 'verified-owner' || isVoting}
|
||||
title={verificationStatus === 'verified-owner' ? "Downvote" : "Full access required to vote"}
|
||||
>
|
||||
<ArrowDown className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-bold mb-2">{post.title}</h2>
|
||||
<p className="text-lg mb-4">{post.content}</p>
|
||||
<div className="flex items-center gap-4 text-xs text-cyber-neutral">
|
||||
<span className="flex items-center">
|
||||
<Clock className="w-3 h-3 mr-1" />
|
||||
{formatDistanceToNow(post.timestamp, { addSuffix: true })}
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<MessageCircle className="w-3 h-3 mr-1" />
|
||||
{postComments.length} {postComments.length === 1 ? 'comment' : 'comments'}
|
||||
</span>
|
||||
<span className="truncate max-w-[150px]">
|
||||
{post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)}
|
||||
</span>
|
||||
<div className="border border-muted rounded-sm p-3 mb-6">
|
||||
<div className="flex gap-3 items-start">
|
||||
<div className="flex flex-col items-center w-6 pt-1">
|
||||
<button
|
||||
className={`p-1 rounded-sm hover:bg-secondary/50 disabled:opacity-50 ${isPostUpvoted ? 'text-primary' : ''}`}
|
||||
onClick={() => handleVotePost(true)}
|
||||
disabled={verificationStatus !== 'verified-owner' || isVoting}
|
||||
title={verificationStatus === 'verified-owner' ? "Upvote" : "Full access required to vote"}
|
||||
>
|
||||
<ArrowUp className="w-5 h-5" />
|
||||
</button>
|
||||
<span className="text-sm font-medium py-1">{post.upvotes.length - post.downvotes.length}</span>
|
||||
<button
|
||||
className={`p-1 rounded-sm hover:bg-secondary/50 disabled:opacity-50 ${isPostDownvoted ? 'text-primary' : ''}`}
|
||||
onClick={() => handleVotePost(false)}
|
||||
disabled={verificationStatus !== 'verified-owner' || isVoting}
|
||||
title={verificationStatus === 'verified-owner' ? "Downvote" : "Full access required to vote"}
|
||||
>
|
||||
<ArrowDown className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-bold mb-2 text-foreground">{post.title}</h2>
|
||||
<p className="text-base mb-4 text-foreground/90">{post.content}</p>
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span className="flex items-center">
|
||||
<Clock className="w-3 h-3 mr-1" />
|
||||
{formatDistanceToNow(post.timestamp, { addSuffix: true })}
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<MessageCircle className="w-3 h-3 mr-1" />
|
||||
{postComments.length} {postComments.length === 1 ? 'comment' : 'comments'}
|
||||
</span>
|
||||
<span className="truncate max-w-[150px]">
|
||||
{post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -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}
|
||||
/>
|
||||
<Button
|
||||
@ -179,19 +174,19 @@ const PostDetail = () => {
|
||||
</form>
|
||||
</div>
|
||||
) : verificationStatus === 'verified-none' ? (
|
||||
<div className="mb-8 p-4 border border-cyber-muted rounded-sm bg-cyber-muted/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Eye className="w-4 h-4 text-cyber-neutral" />
|
||||
<div className="mb-8 p-3 border border-muted rounded-sm bg-secondary/30">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<Eye className="w-4 h-4 text-muted-foreground" />
|
||||
<h3 className="font-medium">Read-Only Mode</h3>
|
||||
</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.
|
||||
You can browse threads but cannot comment or vote.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-8 p-4 border border-cyber-muted rounded-sm bg-cyber-muted/20 text-center">
|
||||
<p className="text-sm mb-3">Connect wallet and verify Ordinal ownership to comment</p>
|
||||
<div className="mb-8 p-3 border border-muted rounded-sm bg-secondary/30 text-center">
|
||||
<p className="text-sm mb-2">Connect wallet and verify Ordinal ownership to comment</p>
|
||||
<Button asChild size="sm">
|
||||
<Link to="/">Go to Home</Link>
|
||||
</Button>
|
||||
@ -200,25 +195,25 @@ const PostDetail = () => {
|
||||
|
||||
<div className="space-y-2">
|
||||
{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>
|
||||
</div>
|
||||
) : (
|
||||
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 flex-col items-center mr-2">
|
||||
<div className="flex flex-col items-center w-5 pt-0.5">
|
||||
<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)}
|
||||
disabled={verificationStatus !== 'verified-owner' || isVoting}
|
||||
title={verificationStatus === 'verified-owner' ? "Upvote" : "Full access required to vote"}
|
||||
>
|
||||
<ArrowUp className="w-4 h-4" />
|
||||
</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
|
||||
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)}
|
||||
disabled={verificationStatus !== 'verified-owner' || isVoting}
|
||||
title={verificationStatus === 'verified-owner' ? "Downvote" : "Full access required to vote"}
|
||||
@ -226,19 +221,19 @@ const PostDetail = () => {
|
||||
<ArrowDown className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-cyber-muted/30 rounded-sm p-3 flex-1">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 pt-0.5">
|
||||
<div className="flex justify-between items-center mb-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<CypherImage
|
||||
src={getIdentityImageUrl(comment.authorAddress)}
|
||||
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)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-cyber-neutral">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatDistanceToNow(comment.timestamp, { addSuffix: true })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 (
|
||||
<div className="min-h-screen flex flex-col bg-cyber-dark text-white">
|
||||
<Header />
|
||||
<main className="flex-1">
|
||||
<main className="flex-1 pt-16">
|
||||
<PostList />
|
||||
</main>
|
||||
<footer className="border-t border-cyber-muted py-4 text-center text-xs text-cyber-neutral">
|
||||
|
||||
16
src/pages/Dashboard.tsx
Normal file
16
src/pages/Dashboard.tsx
Normal 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;
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
import React from 'react';
|
||||
import Header from '@/components/Header';
|
||||
import PostDetail from '@/components/PostDetail';
|
||||
@ -7,7 +6,7 @@ const PostPage = () => {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-cyber-dark text-white">
|
||||
<Header />
|
||||
<main className="flex-1">
|
||||
<main className="flex-1 pt-16">
|
||||
<PostDetail />
|
||||
</main>
|
||||
<footer className="border-t border-cyber-muted py-4 text-center text-xs text-cyber-neutral">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user