OpChan/src/components/PostList.tsx

350 lines
12 KiB
TypeScript
Raw Normal View History

2025-04-15 16:28:03 +05:30
import React, { useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import {
useCell,
useCellPosts,
useForumActions,
usePermissions,
useUserVotes,
useAuth,
} from '@/hooks';
2025-09-05 13:41:37 +05:30
import { EVerificationStatus } from '@/types/identity';
2025-04-15 16:28:03 +05:30
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
2025-04-15 16:28:03 +05:30
import { Textarea } from '@/components/ui/textarea';
import { Skeleton } from '@/components/ui/skeleton';
2025-08-30 18:34:50 +05:30
import {
ArrowLeft,
MessageSquare,
MessageCircle,
ArrowUp,
ArrowDown,
RefreshCw,
Eye,
} from 'lucide-react';
2025-04-15 16:28:03 +05:30
import { formatDistanceToNow } from 'date-fns';
import { CypherImage } from './ui/CypherImage';
2025-04-24 14:31:00 +05:30
import { Badge } from '@/components/ui/badge';
import { AuthorDisplay } from './ui/author-display';
2025-04-15 16:28:03 +05:30
const PostList = () => {
const { cellId } = useParams<{ cellId: string }>();
// ✅ Use reactive hooks for data and actions
const cell = useCell(cellId);
const cellPosts = useCellPosts(cellId, { sortBy: 'relevance' });
2025-08-30 18:34:50 +05:30
const {
createPost,
2025-04-24 14:31:00 +05:30
votePost,
moderatePost,
moderateUser,
refreshData,
isCreatingPost,
isVoting,
} = useForumActions();
const { canPost, canVote, canModerate } = usePermissions();
const userVotes = useUserVotes();
const { currentUser, verificationStatus } = useAuth();
const [newPostTitle, setNewPostTitle] = useState('');
2025-04-15 16:28:03 +05:30
const [newPostContent, setNewPostContent] = useState('');
2025-08-30 18:34:50 +05:30
if (!cellId || cellPosts.isLoading) {
2025-04-15 16:28:03 +05:30
return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="mb-6">
2025-08-30 18:34:50 +05:30
<Link
to="/"
className="text-cyber-accent hover:underline flex items-center gap-1 text-sm"
>
2025-04-15 16:28:03 +05:30
<ArrowLeft className="w-4 h-4" /> Back to Cells
</Link>
</div>
2025-08-30 18:34:50 +05:30
2025-04-15 16:28:03 +05:30
<Skeleton className="h-8 w-32 mb-6 bg-cyber-muted" />
<Skeleton className="h-6 w-64 mb-6 bg-cyber-muted" />
2025-08-30 18:34:50 +05:30
2025-04-15 16:28:03 +05:30
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="border border-cyber-muted rounded-sm p-4">
<div className="mb-2">
<Skeleton className="h-6 w-full mb-2 bg-cyber-muted" />
<Skeleton className="h-6 w-3/4 mb-2 bg-cyber-muted" />
</div>
<Skeleton className="h-4 w-32 bg-cyber-muted" />
</div>
))}
</div>
</div>
);
}
2025-08-30 18:34:50 +05:30
2025-04-15 16:28:03 +05:30
if (!cell) {
return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="mb-6">
2025-08-30 18:34:50 +05:30
<Link
to="/"
className="text-cyber-accent hover:underline flex items-center gap-1 text-sm"
>
2025-04-15 16:28:03 +05:30
<ArrowLeft className="w-4 h-4" /> Back to Cells
</Link>
</div>
<div className="p-8 text-center">
<h1 className="text-2xl font-bold mb-4">Cell Not Found</h1>
2025-08-30 18:34:50 +05:30
<p className="text-cyber-neutral mb-6">
The cell you're looking for doesn't exist.
</p>
2025-04-15 16:28:03 +05:30
<Button asChild>
<Link to="/">Return to Cells</Link>
</Button>
</div>
</div>
);
}
2025-08-30 18:34:50 +05:30
2025-04-15 16:28:03 +05:30
const handleCreatePost = async (e: React.FormEvent) => {
e.preventDefault();
if (!newPostContent.trim()) return;
2025-08-30 18:34:50 +05:30
// ✅ All validation handled in hook
const post = await createPost(cellId, newPostTitle, newPostContent);
if (post) {
setNewPostTitle('');
setNewPostContent('');
2025-04-15 16:28:03 +05:30
}
};
2025-08-30 18:34:50 +05:30
2025-04-24 14:31:00 +05:30
const handleVotePost = async (postId: string, isUpvote: boolean) => {
// ✅ Permission checking handled in hook
2025-04-24 14:31:00 +05:30
await votePost(postId, isUpvote);
};
2025-08-30 18:34:50 +05:30
const getPostVoteType = (postId: string) => {
return userVotes.getPostVoteType(postId);
2025-04-24 14:31:00 +05:30
};
2025-08-30 18:34:50 +05:30
// ✅ Posts already filtered by hook based on user permissions
const visiblePosts = cellPosts.posts;
2025-08-30 18:34:50 +05:30
const handleModerate = async (postId: string) => {
2025-08-30 18:34:50 +05:30
const reason =
window.prompt('Enter a reason for moderation (optional):') || undefined;
if (!cell) return;
// ✅ All validation handled in hook
await moderatePost(cell.id, postId, reason);
};
2025-08-30 18:34:50 +05:30
const handleModerateUser = async (userAddress: string) => {
2025-08-30 18:34:50 +05:30
const reason =
window.prompt('Reason for moderating this user? (optional)') || undefined;
if (!cell) return;
// ✅ All validation handled in hook
await moderateUser(cell.id, userAddress, reason);
};
2025-08-30 18:34:50 +05:30
2025-04-15 16:28:03 +05:30
return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="mb-6">
2025-08-30 18:34:50 +05:30
<Link
to="/"
className="text-cyber-accent hover:underline flex items-center gap-1 text-sm"
>
2025-04-15 16:28:03 +05:30
<ArrowLeft className="w-4 h-4" /> Back to Cells
</Link>
</div>
2025-08-30 18:34:50 +05:30
2025-04-15 16:28:03 +05:30
<div className="flex gap-4 items-start mb-6">
2025-08-30 18:34:50 +05:30
<CypherImage
src={cell.icon}
alt={cell.name}
2025-04-15 16:28:03 +05:30
className="w-12 h-12 object-cover rounded-sm border border-cyber-muted"
generateUniqueFallback={true}
2025-04-15 16:28:03 +05:30
/>
<div className="flex-1">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-glow">{cell.name}</h1>
2025-08-30 18:34:50 +05:30
<Button
variant="outline"
size="icon"
onClick={refreshData}
disabled={cellPosts.isLoading}
title="Refresh data"
>
2025-08-30 18:34:50 +05:30
<RefreshCw
className={`w-4 h-4 ${cellPosts.isLoading ? 'animate-spin' : ''}`}
2025-08-30 18:34:50 +05:30
/>
</Button>
</div>
2025-04-15 16:28:03 +05:30
<p className="text-cyber-neutral">{cell.description}</p>
</div>
</div>
2025-08-30 18:34:50 +05:30
{canPost && (
2025-04-15 16:28:03 +05:30
<div className="mb-8">
<form onSubmit={handleCreatePost}>
<h2 className="text-sm font-bold mb-2 flex items-center gap-1">
<MessageSquare className="w-4 h-4" />
New Thread
</h2>
<div className="mb-3">
<Input
placeholder="Thread title"
value={newPostTitle}
2025-08-30 18:34:50 +05:30
onChange={e => setNewPostTitle(e.target.value)}
className="mb-3 bg-cyber-muted/50 border-cyber-muted"
disabled={isCreatingPost}
/>
<Textarea
placeholder="What's on your mind?"
value={newPostContent}
2025-08-30 18:34:50 +05:30
onChange={e => setNewPostContent(e.target.value)}
className="bg-cyber-muted/50 border-cyber-muted resize-none"
disabled={isCreatingPost}
/>
</div>
2025-04-15 16:28:03 +05:30
<div className="flex justify-end">
2025-08-30 18:34:50 +05:30
<Button
type="submit"
disabled={
isCreatingPost ||
2025-08-30 18:34:50 +05:30
!newPostContent.trim() ||
!newPostTitle.trim()
}
2025-04-15 16:28:03 +05:30
>
{isCreatingPost ? 'Posting...' : 'Post Thread'}
2025-04-15 16:28:03 +05:30
</Button>
</div>
</form>
</div>
)}
2025-08-30 18:34:50 +05:30
2025-09-05 13:41:37 +05:30
{!canPost &&
verificationStatus === EVerificationStatus.WALLET_CONNECTED && (
<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>
2025-04-24 14:31:00 +05:30
</div>
2025-09-05 13:41:37 +05:30
)}
2025-08-30 18:34:50 +05:30
{!canPost && !currentUser && (
2025-04-24 14:31:00 +05:30
<div className="mb-8 p-4 border border-cyber-muted rounded-sm bg-cyber-muted/20 text-center">
2025-08-30 18:34:50 +05:30
<p className="text-sm mb-3">
Connect wallet and verify Ordinal ownership to post
</p>
2025-04-24 14:31:00 +05:30
<Button asChild size="sm">
<Link to="/">Connect Wallet</Link>
</Button>
</div>
)}
2025-08-30 18:34:50 +05:30
2025-04-15 16:28:03 +05:30
<div className="space-y-4">
{visiblePosts.length === 0 ? (
2025-04-15 16:28:03 +05:30
<div className="text-center py-12">
<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>
<p className="text-cyber-neutral">
{canPost
2025-08-30 18:34:50 +05:30
? 'Be the first to post in this cell!'
: 'Connect your wallet and verify Ordinal ownership to start a thread.'}
2025-04-15 16:28:03 +05:30
</p>
</div>
) : (
visiblePosts.map(post => (
2025-08-30 18:34:50 +05:30
<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"
>
2025-04-24 14:31:00 +05:30
<div className="flex gap-4">
<div className="flex flex-col items-center">
2025-08-30 18:34:50 +05:30
<button
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${getPostVoteType(post.id) === 'upvote' ? 'text-cyber-accent' : ''}`}
2025-04-24 14:31:00 +05:30
onClick={() => handleVotePost(post.id, true)}
disabled={!canVote || isVoting}
2025-08-30 18:34:50 +05:30
title={
canVote ? 'Upvote' : 'Connect wallet and verify to vote'
2025-08-30 18:34:50 +05:30
}
2025-04-24 14:31:00 +05:30
>
<ArrowUp className="w-4 h-4" />
</button>
2025-08-30 18:34:50 +05:30
<span className="text-sm py-1">
{post.upvotes.length - post.downvotes.length}
</span>
<button
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${getPostVoteType(post.id) === 'downvote' ? 'text-cyber-accent' : ''}`}
2025-04-24 14:31:00 +05:30
onClick={() => handleVotePost(post.id, false)}
disabled={!canVote || isVoting}
2025-08-30 18:34:50 +05:30
title={
canVote ? 'Downvote' : 'Connect wallet and verify to vote'
2025-08-30 18:34:50 +05:30
}
2025-04-24 14:31:00 +05:30
>
<ArrowDown className="w-4 h-4" />
</button>
</div>
2025-08-30 18:34:50 +05:30
2025-04-24 14:31:00 +05:30
<div className="flex-1">
<Link to={`/post/${post.id}`} className="block">
2025-08-30 18:34:50 +05:30
<h2 className="text-lg font-bold hover:text-cyber-accent">
{post.title}
</h2>
2025-04-24 14:31:00 +05:30
<p className="line-clamp-2 text-sm mb-3">{post.content}</p>
<div className="flex items-center gap-4 text-xs text-cyber-neutral">
2025-08-30 18:34:50 +05:30
<span>
{formatDistanceToNow(post.timestamp, {
addSuffix: true,
})}
</span>
<span>by </span>
2025-08-30 18:34:50 +05:30
<AuthorDisplay
2025-09-03 15:01:57 +05:30
address={post.author}
2025-08-30 18:34:50 +05:30
className="text-xs"
showBadge={false}
/>
2025-04-24 14:31:00 +05:30
</div>
</Link>
{canModerate(cell.id) && !post.moderated && (
2025-08-30 18:34:50 +05:30
<Button
size="sm"
variant="destructive"
className="ml-2"
onClick={() => handleModerate(post.id)}
>
Moderate
</Button>
)}
{canModerate(cell.id) && post.author !== cell.signature && (
2025-08-30 18:34:50 +05:30
<Button
size="sm"
variant="destructive"
className="ml-2"
onClick={() => handleModerateUser(post.author)}
2025-08-30 18:34:50 +05:30
>
Moderate User
</Button>
)}
{post.moderated && (
2025-08-30 18:34:50 +05:30
<span className="ml-2 text-xs text-red-500">
[Moderated]
</span>
)}
2025-04-15 16:28:03 +05:30
</div>
</div>
2025-04-24 14:31:00 +05:30
</div>
2025-04-15 16:28:03 +05:30
))
)}
</div>
</div>
);
};
export default PostList;