mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-07 15:23:05 +00:00
chore: ordinal related improvements
This commit is contained in:
parent
15d6d25455
commit
8129c78e1c
@ -4,11 +4,11 @@ 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 } from 'lucide-react';
|
import { ShieldCheck, LogOut, Terminal, Wifi, WifiOff, Eye, MessageSquare, RefreshCw } from 'lucide-react';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { currentUser, isAuthenticated, connectWallet, disconnectWallet, verifyOrdinal } = useAuth();
|
const { currentUser, isAuthenticated, verificationStatus, connectWallet, disconnectWallet, verifyOrdinal } = useAuth();
|
||||||
const { isNetworkConnected, isRefreshing } = useForum();
|
const { isNetworkConnected, isRefreshing } = useForum();
|
||||||
|
|
||||||
const handleConnect = async () => {
|
const handleConnect = async () => {
|
||||||
@ -22,6 +22,111 @@ const Header = () => {
|
|||||||
const handleVerify = async () => {
|
const handleVerify = async () => {
|
||||||
await verifyOrdinal();
|
await verifyOrdinal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verificationStatus === 'verifying') {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-3 h-3 animate-spin" />
|
||||||
|
<span className="text-xs">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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="border-b border-cyber-muted bg-cyber-dark">
|
<header className="border-b border-cyber-muted bg-cyber-dark">
|
||||||
@ -76,17 +181,7 @@ const Header = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{!isAuthenticated && (
|
{renderAccessBadge()}
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleVerify}
|
|
||||||
className="flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<ShieldCheck className="w-4 h-4" />
|
|
||||||
<span>Verify Ordinal</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<span className="hidden md:flex items-center text-sm text-cyber-neutral px-3">
|
<span className="hidden md:flex items-center text-sm text-cyber-neutral px-3">
|
||||||
{currentUser.address.slice(0, 6)}...{currentUser.address.slice(-4)}
|
{currentUser.address.slice(0, 6)}...{currentUser.address.slice(-4)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -5,10 +5,11 @@ 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 { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { ArrowLeft, ArrowUp, ArrowDown, Clock, MessageCircle, Send, RefreshCw } 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';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
const PostDetail = () => {
|
const PostDetail = () => {
|
||||||
const { postId } = useParams<{ postId: string }>();
|
const { postId } = useParams<{ postId: string }>();
|
||||||
@ -27,34 +28,21 @@ const PostDetail = () => {
|
|||||||
isRefreshing,
|
isRefreshing,
|
||||||
refreshData
|
refreshData
|
||||||
} = useForum();
|
} = useForum();
|
||||||
const { currentUser, isAuthenticated } = useAuth();
|
const { currentUser, isAuthenticated, verificationStatus } = useAuth();
|
||||||
const [newComment, setNewComment] = useState('');
|
const [newComment, setNewComment] = useState('');
|
||||||
|
|
||||||
if (!postId || isInitialLoading) {
|
if (!postId) return <div>Invalid post ID</div>;
|
||||||
|
|
||||||
|
if (isInitialLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
<div className="container mx-auto px-4 py-6">
|
||||||
<div className="mb-6">
|
<Skeleton className="h-6 w-32 mb-6" />
|
||||||
<div className="text-cyber-accent flex items-center gap-1 text-sm">
|
<Skeleton className="h-10 w-3/4 mb-3" />
|
||||||
<ArrowLeft className="w-4 h-4" /> Loading...
|
<Skeleton className="h-32 w-full mb-6" />
|
||||||
</div>
|
<Skeleton className="h-6 w-48 mb-4" />
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border border-cyber-muted rounded-sm p-4 mb-8">
|
|
||||||
<Skeleton className="h-6 w-3/4 mb-2 bg-cyber-muted" />
|
|
||||||
<Skeleton className="h-6 w-1/2 mb-4 bg-cyber-muted" />
|
|
||||||
<Skeleton className="h-4 w-32 bg-cyber-muted" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Skeleton className="h-32 w-full mb-8 bg-cyber-muted" />
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{[...Array(2)].map((_, i) => (
|
<Skeleton className="h-24 w-full" />
|
||||||
<div key={i} className="ml-4 border-l-2 border-cyber-muted pl-4 py-2">
|
<Skeleton className="h-24 w-full" />
|
||||||
<Skeleton className="h-4 w-full mb-2 bg-cyber-muted" />
|
|
||||||
<Skeleton className="h-4 w-3/4 mb-2 bg-cyber-muted" />
|
|
||||||
<Skeleton className="h-3 w-24 bg-cyber-muted" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -64,25 +52,18 @@ const PostDetail = () => {
|
|||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
<div className="container mx-auto px-4 py-6 text-center">
|
||||||
<div className="mb-6">
|
<h2 className="text-xl font-bold mb-4">Post not found</h2>
|
||||||
<Link to="/" className="text-cyber-accent hover:underline flex items-center gap-1 text-sm">
|
<p className="mb-4">The post you're looking for doesn't exist or has been removed.</p>
|
||||||
<ArrowLeft className="w-4 h-4" /> Back to Cells
|
<Button asChild>
|
||||||
</Link>
|
<Link to="/">Go back home</Link>
|
||||||
</div>
|
</Button>
|
||||||
<div className="p-8 text-center">
|
|
||||||
<h1 className="text-2xl font-bold mb-4">Post Not Found</h1>
|
|
||||||
<p className="text-cyber-neutral mb-6">The post you're looking for doesn't exist or may have been pruned.</p>
|
|
||||||
<Button asChild>
|
|
||||||
<Link to="/">Return to Cells</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cell = getCellById(post.cellId);
|
const cell = getCellById(post.cellId);
|
||||||
const postComments = getCommentsByPost(postId);
|
const postComments = getCommentsByPost(post.id);
|
||||||
|
|
||||||
const handleCreateComment = async (e: React.FormEvent) => {
|
const handleCreateComment = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -100,12 +81,12 @@ const PostDetail = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleVotePost = async (isUpvote: boolean) => {
|
const handleVotePost = async (isUpvote: boolean) => {
|
||||||
if (!isAuthenticated) return;
|
if (verificationStatus !== 'verified-owner') return;
|
||||||
await votePost(post.id, isUpvote);
|
await votePost(post.id, isUpvote);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVoteComment = async (commentId: string, isUpvote: boolean) => {
|
const handleVoteComment = async (commentId: string, isUpvote: boolean) => {
|
||||||
if (!isAuthenticated) return;
|
if (verificationStatus !== 'verified-owner') return;
|
||||||
await voteComment(commentId, isUpvote);
|
await voteComment(commentId, isUpvote);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -118,44 +99,39 @@ const PostDetail = () => {
|
|||||||
return votes.some(vote => vote.author === currentUser.address);
|
return votes.some(vote => vote.author === currentUser.address);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getIdentityImageUrl = (address: string) => {
|
||||||
|
return `https://api.dicebear.com/7.x/identicon/svg?seed=${address}`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
<div className="container mx-auto px-4 py-6">
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-6">
|
||||||
<Link
|
|
||||||
to={cell ? `/cell/${cell.id}` : '/'}
|
|
||||||
className="text-cyber-accent hover:underline flex items-center gap-1 text-sm"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
{cell ? `Back to ${cell.name}` : 'Back to Cells'}
|
|
||||||
</Link>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
onClick={() => navigate(`/cell/${post.cellId}`)}
|
||||||
size="icon"
|
variant="ghost"
|
||||||
onClick={refreshData}
|
size="sm"
|
||||||
disabled={isRefreshing}
|
className="mb-4"
|
||||||
title="Refresh data"
|
|
||||||
>
|
>
|
||||||
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
|
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||||
|
Back to /{cell?.name || 'cell'}/
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
|
<div className="flex gap-4 items-start">
|
||||||
<div className="border border-cyber-muted rounded-sm p-4 mb-8">
|
<div className="flex flex-col items-center">
|
||||||
<div className="flex gap-2 items-start">
|
|
||||||
<div className="flex flex-col items-center mr-2">
|
|
||||||
<button
|
<button
|
||||||
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${isPostUpvoted ? 'text-cyber-accent' : ''}`}
|
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${isPostUpvoted ? 'text-cyber-accent' : ''}`}
|
||||||
onClick={() => handleVotePost(true)}
|
onClick={() => handleVotePost(true)}
|
||||||
disabled={!isAuthenticated || isVoting}
|
disabled={verificationStatus !== 'verified-owner' || isVoting}
|
||||||
title={isAuthenticated ? "Upvote" : "Verify Ordinal to vote"}
|
title={verificationStatus === 'verified-owner' ? "Upvote" : "Full access required to vote"}
|
||||||
>
|
>
|
||||||
<ArrowUp className="w-5 h-5" />
|
<ArrowUp className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<span className="text-xs py-1">{post.upvotes.length - post.downvotes.length}</span>
|
<span className="text-sm py-1">{post.upvotes.length - post.downvotes.length}</span>
|
||||||
<button
|
<button
|
||||||
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${isPostDownvoted ? 'text-cyber-accent' : ''}`}
|
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${isPostDownvoted ? 'text-cyber-accent' : ''}`}
|
||||||
onClick={() => handleVotePost(false)}
|
onClick={() => handleVotePost(false)}
|
||||||
disabled={!isAuthenticated || isVoting}
|
disabled={verificationStatus !== 'verified-owner' || isVoting}
|
||||||
title={isAuthenticated ? "Downvote" : "Verify Ordinal to vote"}
|
title={verificationStatus === 'verified-owner' ? "Downvote" : "Full access required to vote"}
|
||||||
>
|
>
|
||||||
<ArrowDown className="w-5 h-5" />
|
<ArrowDown className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
@ -181,7 +157,7 @@ const PostDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAuthenticated ? (
|
{verificationStatus === 'verified-owner' ? (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<form onSubmit={handleCreateComment}>
|
<form onSubmit={handleCreateComment}>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@ -202,6 +178,17 @@ const PostDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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" />
|
||||||
|
<h3 className="font-medium">Read-Only Mode</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-cyber-neutral">
|
||||||
|
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">
|
<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>
|
<p className="text-sm mb-3">Connect wallet and verify Ordinal ownership to comment</p>
|
||||||
@ -224,8 +211,8 @@ const PostDetail = () => {
|
|||||||
<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-cyber-muted/50 ${isCommentVoted(comment, true) ? 'text-cyber-accent' : ''}`}
|
||||||
onClick={() => handleVoteComment(comment.id, true)}
|
onClick={() => handleVoteComment(comment.id, true)}
|
||||||
disabled={!isAuthenticated || isVoting}
|
disabled={verificationStatus !== 'verified-owner' || isVoting}
|
||||||
title={isAuthenticated ? "Upvote" : "Verify Ordinal 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>
|
||||||
@ -233,24 +220,29 @@ const PostDetail = () => {
|
|||||||
<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-cyber-muted/50 ${isCommentVoted(comment, false) ? 'text-cyber-accent' : ''}`}
|
||||||
onClick={() => handleVoteComment(comment.id, false)}
|
onClick={() => handleVoteComment(comment.id, false)}
|
||||||
disabled={!isAuthenticated || isVoting}
|
disabled={verificationStatus !== 'verified-owner' || isVoting}
|
||||||
title={isAuthenticated ? "Downvote" : "Verify Ordinal to vote"}
|
title={verificationStatus === 'verified-owner' ? "Downvote" : "Full access required to vote"}
|
||||||
>
|
>
|
||||||
<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">
|
<div className="flex justify-between items-start mb-2">
|
||||||
<p className="text-sm mb-2">{comment.content}</p>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-3 text-xs text-cyber-neutral">
|
<CypherImage
|
||||||
<span className="flex items-center">
|
src={getIdentityImageUrl(comment.authorAddress)}
|
||||||
<Clock className="w-3 h-3 mr-1" />
|
alt={comment.authorAddress.slice(0, 6)}
|
||||||
|
className="rounded-sm w-5 h-5 bg-cyber-muted"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-cyber-neutral">
|
||||||
|
{comment.authorAddress.slice(0, 6)}...{comment.authorAddress.slice(-4)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-cyber-neutral">
|
||||||
{formatDistanceToNow(comment.timestamp, { addSuffix: true })}
|
{formatDistanceToNow(comment.timestamp, { addSuffix: true })}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate max-w-[120px]">
|
|
||||||
{comment.authorAddress.slice(0, 6)}...{comment.authorAddress.slice(-4)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-sm break-words">{comment.content}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,9 +6,10 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { ArrowLeft, MessageSquare, MessageCircle, ArrowUp, ArrowDown, Clock, RefreshCw } from 'lucide-react';
|
import { ArrowLeft, MessageSquare, MessageCircle, ArrowUp, ArrowDown, Clock, RefreshCw, Eye } from 'lucide-react';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { CypherImage } from './ui/CypherImage';
|
import { CypherImage } from './ui/CypherImage';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
const PostList = () => {
|
const PostList = () => {
|
||||||
const { cellId } = useParams<{ cellId: string }>();
|
const { cellId } = useParams<{ cellId: string }>();
|
||||||
@ -20,9 +21,12 @@ const PostList = () => {
|
|||||||
isInitialLoading,
|
isInitialLoading,
|
||||||
isPostingPost,
|
isPostingPost,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
refreshData
|
refreshData,
|
||||||
|
votePost,
|
||||||
|
isVoting,
|
||||||
|
posts
|
||||||
} = useForum();
|
} = useForum();
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated, currentUser, verificationStatus } = useAuth();
|
||||||
const [newPostTitle, setNewPostTitle] = useState('');
|
const [newPostTitle, setNewPostTitle] = useState('');
|
||||||
const [newPostContent, setNewPostContent] = useState('');
|
const [newPostContent, setNewPostContent] = useState('');
|
||||||
|
|
||||||
@ -54,7 +58,7 @@ const PostList = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cell = getCellById(cellId);
|
const cell = getCellById(cellId);
|
||||||
const posts = getPostsByCell(cellId);
|
const cellPosts = getPostsByCell(cellId);
|
||||||
|
|
||||||
if (!cell) {
|
if (!cell) {
|
||||||
return (
|
return (
|
||||||
@ -91,6 +95,19 @@ const PostList = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleVotePost = async (postId: string, isUpvote: boolean) => {
|
||||||
|
if (!isAuthenticated) return;
|
||||||
|
await votePost(postId, isUpvote);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPostVoted = (postId: string, isUpvote: boolean) => {
|
||||||
|
if (!currentUser) return false;
|
||||||
|
const post = posts.find(p => p.id === postId);
|
||||||
|
if (!post) return false;
|
||||||
|
const votes = isUpvote ? post.upvotes : post.downvotes;
|
||||||
|
return votes.some(vote => vote.author === currentUser.address);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@ -123,7 +140,7 @@ const PostList = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAuthenticated && (
|
{verificationStatus === 'verified-owner' && (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<form onSubmit={handleCreatePost}>
|
<form onSubmit={handleCreatePost}>
|
||||||
<h2 className="text-sm font-bold mb-2 flex items-center gap-1">
|
<h2 className="text-sm font-bold mb-2 flex items-center gap-1">
|
||||||
@ -158,8 +175,30 @@ const PostList = () => {
|
|||||||
</div>
|
</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" />
|
||||||
|
<h3 className="font-medium">Read-Only Mode</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-cyber-neutral mb-2">
|
||||||
|
Your wallet does not contain any Ordinal Operators. You can browse threads but cannot post or interact.
|
||||||
|
</p>
|
||||||
|
<Badge variant="outline" className="text-xs">No Ordinals Found</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!currentUser && (
|
||||||
|
<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 post</p>
|
||||||
|
<Button asChild size="sm">
|
||||||
|
<Link to="/">Connect Wallet</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{posts.length === 0 ? (
|
{cellPosts.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<MessageCircle className="w-12 h-12 mx-auto mb-4 text-cyber-neutral opacity-50" />
|
<MessageCircle className="w-12 h-12 mx-auto mb-4 text-cyber-neutral opacity-50" />
|
||||||
<h2 className="text-xl font-bold mb-2">No Threads Yet</h2>
|
<h2 className="text-xl font-bold mb-2">No Threads Yet</h2>
|
||||||
@ -170,32 +209,41 @@ const PostList = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
posts.map(post => (
|
cellPosts.map(post => (
|
||||||
<Link to={`/post/${post.id}`} key={post.id} className="thread-card block">
|
<div key={post.id} className="post-card p-4 border border-cyber-muted rounded-sm bg-cyber-muted/20 hover:bg-cyber-muted/30 transition duration-200">
|
||||||
<div>
|
<div className="flex gap-4">
|
||||||
<h3 className="font-medium mb-1">{post.title}</h3>
|
<div className="flex flex-col items-center">
|
||||||
<p className="text-sm mb-3">{post.content}</p>
|
<button
|
||||||
<div className="flex items-center gap-4 text-xs text-cyber-neutral">
|
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${isPostVoted(post.id, true) ? 'text-cyber-accent' : ''}`}
|
||||||
<span className="flex items-center">
|
onClick={() => handleVotePost(post.id, true)}
|
||||||
<Clock className="w-3 h-3 mr-1" />
|
disabled={!isAuthenticated || isVoting}
|
||||||
{formatDistanceToNow(post.timestamp, { addSuffix: true })}
|
title={isAuthenticated ? "Upvote" : "Verify Ordinal to vote"}
|
||||||
</span>
|
>
|
||||||
<span className="flex items-center">
|
<ArrowUp className="w-4 h-4" />
|
||||||
<MessageCircle className="w-3 h-3 mr-1" />
|
</button>
|
||||||
{getCommentsByPost(post.id).length} comments
|
<span className="text-sm py-1">{post.upvotes.length - post.downvotes.length}</span>
|
||||||
</span>
|
<button
|
||||||
<div className="flex items-center">
|
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${isPostVoted(post.id, false) ? 'text-cyber-accent' : ''}`}
|
||||||
<ArrowUp className="w-3 h-3 mr-1" />
|
onClick={() => handleVotePost(post.id, false)}
|
||||||
<span>{post.upvotes.length}</span>
|
disabled={!isAuthenticated || isVoting}
|
||||||
<ArrowDown className="w-3 h-3 mx-1" />
|
title={isAuthenticated ? "Downvote" : "Verify Ordinal to vote"}
|
||||||
<span>{post.downvotes.length}</span>
|
>
|
||||||
</div>
|
<ArrowDown className="w-4 h-4" />
|
||||||
<span className="truncate max-w-[120px]">
|
</button>
|
||||||
{post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)}
|
</div>
|
||||||
</span>
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<Link to={`/post/${post.id}`} className="block">
|
||||||
|
<h2 className="text-lg font-bold hover:text-cyber-accent">{post.title}</h2>
|
||||||
|
<p className="line-clamp-2 text-sm mb-3">{post.content}</p>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-cyber-neutral">
|
||||||
|
<span>{formatDistanceToNow(post.timestamp, { addSuffix: true })}</span>
|
||||||
|
<span>by {post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)}</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,10 +3,13 @@ import { useToast } from '@/components/ui/use-toast';
|
|||||||
import { User } from '@/types';
|
import { User } from '@/types';
|
||||||
import { OrdinalAPI } from '@/lib/identity/ordinal';
|
import { OrdinalAPI } from '@/lib/identity/ordinal';
|
||||||
|
|
||||||
|
export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-owner' | 'verifying';
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
currentUser: User | null;
|
currentUser: User | null;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
isAuthenticating: boolean;
|
isAuthenticating: boolean;
|
||||||
|
verificationStatus: VerificationStatus;
|
||||||
connectWallet: () => Promise<void>;
|
connectWallet: () => Promise<void>;
|
||||||
disconnectWallet: () => void;
|
disconnectWallet: () => void;
|
||||||
verifyOrdinal: () => Promise<boolean>;
|
verifyOrdinal: () => Promise<boolean>;
|
||||||
@ -17,28 +20,34 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|||||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||||
|
const [verificationStatus, setVerificationStatus] = useState<VerificationStatus>('unverified');
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const ordinalApi = new OrdinalAPI();
|
const ordinalApi = new OrdinalAPI();
|
||||||
|
|
||||||
// Check for existing session on mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedUser = localStorage.getItem('opchan-user');
|
const storedUser = localStorage.getItem('opchan-user');
|
||||||
if (storedUser) {
|
if (storedUser) {
|
||||||
try {
|
try {
|
||||||
const user = JSON.parse(storedUser);
|
const user = JSON.parse(storedUser);
|
||||||
// Check if the stored authentication is still valid (not expired)
|
|
||||||
const lastChecked = user.lastChecked || 0;
|
const lastChecked = user.lastChecked || 0;
|
||||||
const expiryTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
const expiryTime = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
if (Date.now() - lastChecked < expiryTime) {
|
if (Date.now() - lastChecked < expiryTime) {
|
||||||
setCurrentUser(user);
|
setCurrentUser(user);
|
||||||
|
|
||||||
|
if ('ordinalOwnership' in user) {
|
||||||
|
setVerificationStatus(user.ordinalOwnership ? 'verified-owner' : 'verified-none');
|
||||||
|
} else {
|
||||||
|
setVerificationStatus('unverified');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Clear expired session
|
|
||||||
localStorage.removeItem('opchan-user');
|
localStorage.removeItem('opchan-user');
|
||||||
|
setVerificationStatus('unverified');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to parse stored user data", e);
|
console.error("Failed to parse stored user data", e);
|
||||||
localStorage.removeItem('opchan-user');
|
localStorage.removeItem('opchan-user');
|
||||||
|
setVerificationStatus('unverified');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@ -59,6 +68,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
// Store user data
|
// Store user data
|
||||||
setCurrentUser(newUser);
|
setCurrentUser(newUser);
|
||||||
localStorage.setItem('opchan-user', JSON.stringify(newUser));
|
localStorage.setItem('opchan-user', JSON.stringify(newUser));
|
||||||
|
setVerificationStatus('unverified');
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Wallet Connected",
|
title: "Wallet Connected",
|
||||||
@ -82,6 +92,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const disconnectWallet = () => {
|
const disconnectWallet = () => {
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
localStorage.removeItem('opchan-user');
|
localStorage.removeItem('opchan-user');
|
||||||
|
setVerificationStatus('unverified');
|
||||||
toast({
|
toast({
|
||||||
title: "Disconnected",
|
title: "Disconnected",
|
||||||
description: "Your wallet has been disconnected.",
|
description: "Your wallet has been disconnected.",
|
||||||
@ -99,8 +110,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsAuthenticating(true);
|
setIsAuthenticating(true);
|
||||||
|
setVerificationStatus('verifying');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
toast({ title: "Verifying Ordinal", description: "Checking your wallet for Ordinal Operators..." });
|
toast({
|
||||||
|
title: "Verifying Ordinal",
|
||||||
|
description: "Checking your wallet for Ordinal Operators..."
|
||||||
|
});
|
||||||
|
|
||||||
const response = await ordinalApi.getOperatorDetails(currentUser.address);
|
const response = await ordinalApi.getOperatorDetails(currentUser.address);
|
||||||
const hasOperators = response.has_operators;
|
const hasOperators = response.has_operators;
|
||||||
@ -114,31 +130,38 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setCurrentUser(updatedUser);
|
setCurrentUser(updatedUser);
|
||||||
localStorage.setItem('opchan-user', JSON.stringify(updatedUser));
|
localStorage.setItem('opchan-user', JSON.stringify(updatedUser));
|
||||||
|
|
||||||
|
// Update verification status
|
||||||
|
setVerificationStatus(hasOperators ? 'verified-owner' : 'verified-none');
|
||||||
|
|
||||||
if (hasOperators) {
|
if (hasOperators) {
|
||||||
toast({
|
toast({
|
||||||
title: "Ordinal Verified",
|
title: "Ordinal Verified",
|
||||||
description: "You can now post and interact with the forum.",
|
description: "You now have full access to post and interact with the forum.",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
title: "Verification Failed",
|
title: "Read-Only Access",
|
||||||
description: "No Ordinal Operators found in the connected wallet.",
|
description: "No Ordinal Operators found. You have read-only access.",
|
||||||
variant: "destructive",
|
variant: "default",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasOperators;
|
return hasOperators;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error verifying Ordinal:", error);
|
console.error("Error verifying Ordinal:", error);
|
||||||
|
setVerificationStatus('unverified');
|
||||||
|
|
||||||
let errorMessage = "Failed to verify Ordinal ownership. Please try again.";
|
let errorMessage = "Failed to verify Ordinal ownership. Please try again.";
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
errorMessage = error.message;
|
errorMessage = error.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Verification Error",
|
title: "Verification Error",
|
||||||
description: errorMessage,
|
description: errorMessage,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setIsAuthenticating(false);
|
setIsAuthenticating(false);
|
||||||
@ -151,6 +174,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
currentUser,
|
currentUser,
|
||||||
isAuthenticated: !!currentUser?.ordinalOwnership,
|
isAuthenticated: !!currentUser?.ordinalOwnership,
|
||||||
isAuthenticating,
|
isAuthenticating,
|
||||||
|
verificationStatus,
|
||||||
connectWallet,
|
connectWallet,
|
||||||
disconnectWallet,
|
disconnectWallet,
|
||||||
verifyOrdinal,
|
verifyOrdinal,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user