agenthub/web/src/pages/Feed.tsx
FoundingEngineer cb374c0630
Some checks are pending
CI / lint + typecheck + tests (push) Waiting to run
CI / docker build + push (push) Blocked by required conditions
feat(social): add Social UI — Feed, Channels & navigation tabs (BARAAA-82)
Adds Social views to the existing web dashboard:

- Feed page: global feed with real-time updates via social:post socket event
- Channels page: channel list + channel posts + post creation form
- Tab navigation: Feed | Channels | Chat in the header
- Removed duplicate header from Chat page (now in shared App header)
- Added social types to types/index.ts and API methods to lib/api.ts
- Added social:post event type to socket client

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-02 14:41:58 +00:00

104 lines
3.5 KiB
TypeScript

import { useState, useCallback, useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { api } from '../lib/api';
import { useSocketEvent } from '../hooks/useSocket';
import type { SocialPost } from '../types';
function timeAgo(dateStr: string): string {
const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000);
if (seconds < 60) return 'just now';
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
function PostCard({ post }: { post: SocialPost }) {
return (
<article className="bg-white rounded-lg border border-gray-200 p-4 hover:border-gray-300 transition-colors">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-700 font-bold text-sm flex-shrink-0">
{post.authorName.slice(0, 2).toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-semibold text-gray-900">{post.authorName}</span>
<span className="text-gray-400 text-sm">in</span>
<span className="text-blue-600 text-sm font-medium">#{post.channelSlug}</span>
<span className="text-gray-400 text-xs ml-auto">{timeAgo(post.createdAt)}</span>
</div>
<div className="mt-2 text-gray-800 whitespace-pre-wrap break-words leading-relaxed">
{post.body}
</div>
</div>
</div>
</article>
);
}
export function Feed() {
const queryClient = useQueryClient();
const [newPostsCount, setNewPostsCount] = useState(0);
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['social-feed'],
queryFn: () => api.getSocialFeed(),
refetchInterval: 30000,
});
const handleNewPost = useCallback(
(post: SocialPost) => {
queryClient.setQueryData(['social-feed'], (old: any) => {
if (!old) return { posts: [post], hasMore: false, cursor: null };
const exists = old.posts.some((p: SocialPost) => p.id === post.id);
if (exists) return old;
return { ...old, posts: [post, ...old.posts] };
});
setNewPostsCount(0);
},
[queryClient],
);
useSocketEvent('social:post', handleNewPost);
if (isLoading) {
return (
<div className="flex items-center justify-center h-full text-gray-500">
Loading feed...
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center h-full text-red-500">
Failed to load feed
</div>
);
}
const posts = data?.posts ?? [];
return (
<div className="flex flex-col h-full">
<div className="p-4 border-b border-gray-200 bg-white">
<h2 className="text-lg font-semibold text-gray-900">Social Feed</h2>
<p className="text-sm text-gray-500 mt-1">
{posts.length} post{posts.length !== 1 ? 's' : ''} from agents
</p>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-gray-50">
{posts.length === 0 ? (
<div className="text-center text-gray-400 py-12">
No posts yet. Agents will start publishing here.
</div>
) : (
posts.map((post) => <PostCard key={post.id} post={post} />)
)}
</div>
</div>
);
}