OpChan/src/components/ActivityFeed.tsx
2025-09-03 15:56:00 +05:30

190 lines
5.5 KiB
TypeScript

import React from 'react';
import { useForumData } from '@/hooks';
import { Link } from 'react-router-dom';
import { formatDistanceToNow } from 'date-fns';
import { Skeleton } from '@/components/ui/skeleton';
import { MessageSquareText, Newspaper } from 'lucide-react';
import { AuthorDisplay } from './ui/author-display';
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 = () => {
// ✅ Use reactive hooks for data
const forumData = useForumData();
const {
postsWithVoteStatus,
commentsWithVoteStatus,
cellsWithStats,
isInitialLoading,
} = forumData;
// ✅ Use pre-computed data with vote scores
const combinedFeed: FeedItem[] = [
...postsWithVoteStatus.map(
(post): PostFeedItem => ({
id: post.id,
type: 'post',
timestamp: post.timestamp,
ownerAddress: post.author,
title: post.title,
cellId: post.cellId,
postId: post.id,
commentCount: forumData.commentsByPost[post.id]?.length || 0,
voteCount: post.voteScore,
})
),
...commentsWithVoteStatus
.map((comment): CommentFeedItem | null => {
const parentPost = postsWithVoteStatus.find(
p => p.id === comment.postId
);
if (!parentPost) return null;
return {
id: comment.id,
type: 'comment',
timestamp: comment.timestamp,
ownerAddress: comment.author,
content: comment.content,
postId: comment.postId,
cellId: parentPost.cellId,
voteCount: comment.voteScore,
};
})
.filter((item): item is CommentFeedItem => item !== null),
].sort((a, b) => b.timestamp - a.timestamp);
const renderFeedItem = (item: FeedItem) => {
const cell = item.cellId
? cellsWithStats.find(c => c.id === item.cellId)
: undefined;
const timeAgo = formatDistanceToNow(new Date(item.timestamp), {
addSuffix: true,
});
const linkTarget =
item.type === 'post' ? `/post/${item.id}` : `/post/${item.postId}`;
return (
<div
key={item.id}
className="border border-cyber-muted rounded-sm p-3 bg-cyber-muted/10 hover:bg-cyber-muted/20 transition-colors"
>
<div className="flex items-start space-x-3">
<div className="flex-shrink-0">
{item.type === 'post' ? (
<Newspaper className="w-5 h-5 text-cyber-accent" />
) : (
<MessageSquareText className="w-5 h-5 text-blue-400" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 text-xs text-muted-foreground mb-1">
<AuthorDisplay
address={item.ownerAddress}
className="text-xs"
showBadge={false}
/>
<span></span>
<span>{timeAgo}</span>
{cell && (
<>
<span></span>
<span className="text-cyber-accent">r/{cell.name}</span>
</>
)}
</div>
<Link to={linkTarget} className="block hover:opacity-80">
{item.type === 'post' ? (
<div>
<div className="font-medium text-sm mb-1 line-clamp-2">
{item.title}
</div>
<div className="flex items-center space-x-4 text-xs text-muted-foreground">
<span> {item.voteCount}</span>
<span>{item.commentCount} comments</span>
</div>
</div>
) : (
<div>
<div className="text-sm line-clamp-3 mb-1">
{item.content}
</div>
<div className="text-xs text-muted-foreground">
{item.voteCount} Reply to post
</div>
</div>
)}
</Link>
</div>
</div>
</div>
);
};
if (isInitialLoading) {
return (
<div className="space-y-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="border border-cyber-muted rounded-sm p-3">
<div className="flex items-start space-x-3">
<Skeleton className="w-5 h-5 rounded bg-cyber-muted" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-3/4 bg-cyber-muted" />
<Skeleton className="h-3 w-1/2 bg-cyber-muted" />
</div>
</div>
</div>
))}
</div>
);
}
if (combinedFeed.length === 0) {
return (
<div className="text-center py-8">
<MessageSquareText className="w-12 h-12 mx-auto mb-4 text-muted-foreground opacity-50" />
<h3 className="text-lg font-bold mb-2">No Activity Yet</h3>
<p className="text-muted-foreground">
Be the first to create a post or comment!
</p>
</div>
);
}
return (
<div className="space-y-3">
{combinedFeed.slice(0, 20).map(renderFeedItem)}
</div>
);
};
export default ActivityFeed;