OpChan/src/components/PostCard.tsx

235 lines
7.7 KiB
TypeScript
Raw Normal View History

import React from 'react';
import { Link } from 'react-router-dom';
2025-09-05 17:24:29 +05:30
import { ArrowUp, ArrowDown, MessageSquare, Clipboard } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
2025-08-30 18:34:50 +05:30
import { Post } from '@/types/forum';
import {
useForumActions,
usePermissions,
useUserVotes,
useForumData,
2025-09-05 17:24:29 +05:30
usePostBookmark,
} from '@/hooks';
import { RelevanceIndicator } from '@/components/ui/relevance-indicator';
import { AuthorDisplay } from '@/components/ui/author-display';
2025-09-05 17:24:29 +05:30
import { BookmarkButton } from '@/components/ui/bookmark-button';
2025-09-10 15:04:24 +05:30
import { LinkRenderer } from '@/components/ui/link-renderer';
2025-09-04 13:27:47 +05:30
import { usePending, usePendingVote } from '@/hooks/usePending';
2025-09-05 17:24:29 +05:30
import { useToast } from '@/components/ui/use-toast';
interface PostCardProps {
post: Post;
commentCount?: number;
}
const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
const { cellsWithStats } = useForumData();
const { votePost, isVoting } = useForumActions();
const { canVote } = usePermissions();
const userVotes = useUserVotes();
2025-09-05 17:24:29 +05:30
const {
isBookmarked,
loading: bookmarkLoading,
toggleBookmark,
} = usePostBookmark(post, post.cellId);
const { toast } = useToast();
2025-08-30 18:34:50 +05:30
// ✅ Get pre-computed cell data
const cell = cellsWithStats.find(c => c.id === post.cellId);
const cellName = cell?.name || 'unknown';
2025-08-30 18:34:50 +05:30
// ✅ Use pre-computed vote data (assuming post comes from useForumData)
const score =
'voteScore' in post
? (post.voteScore as number)
: post.upvotes.length - post.downvotes.length;
2025-09-04 13:27:47 +05:30
const { isPending } = usePending(post.id);
const votePending = usePendingVote(post.id);
2025-08-30 18:34:50 +05:30
// ✅ Get user vote status from hook
const userVoteType = userVotes.getPostVoteType(post.id);
const userUpvoted = userVoteType === 'upvote';
const userDownvoted = userVoteType === 'downvote';
2025-08-30 18:34:50 +05:30
// ✅ Content truncation (simple presentation logic is OK)
2025-08-30 18:34:50 +05:30
const contentPreview =
post.content.length > 200
? post.content.substring(0, 200) + '...'
: post.content;
const handleVote = async (e: React.MouseEvent, isUpvote: boolean) => {
e.preventDefault();
// ✅ All validation and permission checking handled in hook
await votePost(post.id, isUpvote);
};
2025-09-05 17:24:29 +05:30
const handleBookmark = async (e?: React.MouseEvent) => {
if (e) {
e.preventDefault();
e.stopPropagation();
}
await toggleBookmark();
};
const handleShare = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const postUrl = `${window.location.origin}/post/${post.id}`;
try {
await navigator.clipboard.writeText(postUrl);
toast({
title: 'Link copied!',
description: 'Post link has been copied to your clipboard.',
});
} catch {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = postUrl;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
toast({
title: 'Link copied!',
description: 'Post link has been copied to your clipboard.',
});
}
};
return (
2025-09-05 20:26:29 +05:30
<div className="thread-card mb-2">
<div className="flex">
{/* Voting column */}
<div className="flex flex-col items-center p-2 bg-cyber-muted/50 border-r border-cyber-muted">
2025-08-30 18:34:50 +05:30
<button
className={`p-1 rounded hover:bg-cyber-muted transition-colors ${
2025-08-30 18:34:50 +05:30
userUpvoted
? 'text-cyber-accent'
: 'text-cyber-neutral hover:text-cyber-accent'
}`}
2025-08-30 18:34:50 +05:30
onClick={e => handleVote(e, true)}
disabled={!canVote || isVoting}
title={canVote ? 'Upvote' : 'Connect wallet and verify to vote'}
>
<ArrowUp className="w-5 h-5" />
</button>
2025-08-30 18:34:50 +05:30
<span
className={`text-sm font-medium px-1 ${
score > 0
? 'text-cyber-accent'
: score < 0
? 'text-blue-400'
: 'text-cyber-neutral'
}`}
>
{score}
</span>
2025-08-30 18:34:50 +05:30
<button
className={`p-1 rounded hover:bg-cyber-muted transition-colors ${
2025-08-30 18:34:50 +05:30
userDownvoted
? 'text-blue-400'
: 'text-cyber-neutral hover:text-blue-400'
}`}
2025-08-30 18:34:50 +05:30
onClick={e => handleVote(e, false)}
disabled={!canVote || isVoting}
title={canVote ? 'Downvote' : 'Connect wallet and verify to vote'}
>
<ArrowDown className="w-5 h-5" />
</button>
2025-09-04 13:27:47 +05:30
{votePending.isPending && (
<span className="mt-1 text-[10px] text-yellow-400">syncing</span>
)}
</div>
{/* Content column */}
<div className="flex-1 p-3">
2025-09-10 16:57:38 +05:30
<div className="block hover:opacity-80">
{/* Post metadata */}
<div className="flex items-center text-xs text-cyber-neutral mb-2 space-x-2">
2025-08-30 18:34:50 +05:30
<span className="font-medium text-cyber-accent">
r/{cellName}
</span>
<span></span>
<span>Posted by u/</span>
2025-08-30 18:34:50 +05:30
<AuthorDisplay
2025-09-03 15:01:57 +05:30
address={post.author}
className="text-xs"
showBadge={false}
/>
<span></span>
2025-08-30 18:34:50 +05:30
<span>
{formatDistanceToNow(new Date(post.timestamp), {
addSuffix: true,
})}
</span>
{post.relevanceScore !== undefined && (
<>
<span></span>
2025-08-30 18:34:50 +05:30
<RelevanceIndicator
score={post.relevanceScore}
details={post.relevanceDetails}
type="post"
className="text-xs"
showTooltip={true}
/>
</>
)}
</div>
2025-09-10 16:57:38 +05:30
{/* Post title and content - clickable to navigate to post */}
2025-09-10 17:06:01 +05:30
<div className="block">
<Link to={`/post/${post.id}`} className="block">
<h2 className="text-lg font-semibold text-glow mb-2 hover:text-cyber-accent transition-colors">
{post.title}
</h2>
</Link>
2025-09-10 16:57:38 +05:30
{/* Post content preview */}
<p className="text-cyber-neutral text-sm leading-relaxed mb-3">
<LinkRenderer text={contentPreview} />
</p>
2025-09-10 17:06:01 +05:30
</div>
{/* Post actions */}
2025-09-05 17:24:29 +05:30
<div className="flex items-center justify-between text-xs text-cyber-neutral">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-1 hover:text-cyber-accent transition-colors">
<MessageSquare className="w-4 h-4" />
<span>{commentCount} comments</span>
</div>
{isPending && (
<span className="px-2 py-0.5 rounded-sm bg-yellow-500/20 text-yellow-400 border border-yellow-500/30">
syncing
</span>
)}
<button
onClick={handleShare}
className="hover:text-cyber-accent transition-colors flex items-center gap-1"
title="Copy link"
>
<Clipboard size={14} />
Share
</button>
</div>
2025-09-05 17:24:29 +05:30
<BookmarkButton
isBookmarked={isBookmarked}
loading={bookmarkLoading}
onClick={handleBookmark}
size="sm"
variant="ghost"
/>
</div>
2025-09-10 16:57:38 +05:30
</div>
</div>
</div>
</div>
);
};
2025-08-30 18:34:50 +05:30
export default PostCard;