import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Card, Badge, Table, Avatar, Button, Space, Tag, Typography, Select, Drawer, Form, Empty, Spin } from 'antd'; import { UserOutlined, VerifiedOutlined, ShopOutlined, FilterOutlined, YoutubeOutlined, InstagramOutlined, LinkedinOutlined, FacebookOutlined, GlobalOutlined, MessageOutlined } from '@ant-design/icons'; import { format } from 'date-fns'; import supabase from '../utils/supabase'; // API response type definition based on backend structure interface ApiPost { post_id: string; title: string; description: string; platform: string; post_url: string; published_at: string; updated_at: string; influencer_id: string; views_count?: number; likes_count?: number; comments_count?: number; shares_count?: number; influencer?: { influencer_id: string; name: string; platform: string; profile_url: string; followers_count: number; }; } // Frontend representation of a post interface FrontendPost { id: string; title: string; description: string; author: string; authorType: 'influencer' | 'brand' | 'official'; platform: 'youtube' | 'instagram' | 'facebook' | 'linkedin' | 'tiktok'; contentType: 'post' | 'video' | 'reel' | 'short'; timestamp: string; engagement: { views?: number; likes?: number; comments?: number; shares?: number; }; url?: string; } interface PostListProps { influencerId?: string; // Optional influencer ID to filter posts by influencer projectId?: string; // Optional project ID to filter posts by project } const { Text, Title } = Typography; const { Option } = Select; const PostList: React.FC = ({ influencerId, projectId }) => { const navigate = useNavigate(); const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedPost, setSelectedPost] = useState(null); const [platformFilter, setPlatformFilter] = useState('all'); const [contentTypeFilter, setContentTypeFilter] = useState('all'); const [showFilters, setShowFilters] = useState(false); const [totalPosts, setTotalPosts] = useState(0); const [offset, setOffset] = useState(0); const [hasMore, setHasMore] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); const observer = useRef(null); const loadMoreRef = useRef(null); // Fetch posts data const fetchPosts = async () => { setLoading(true); setError(null); setPosts([]); setOffset(0); setHasMore(true); try { // Build query parameters const params = new URLSearchParams(); const pageSize = 20; params.append('limit', pageSize.toString()); params.append('offset', '0'); params.append('sort', 'published_at'); params.append('order', 'desc'); if (platformFilter && platformFilter !== 'all') { params.append('platform', platformFilter); } if (influencerId) { params.append('influencer_id', influencerId); } if (projectId) { params.append('project_id', projectId); } // Get auth token from Supabase const { data: { session } } = await supabase.auth.getSession(); const token = session?.access_token; if (!token) { console.error('Authentication token not found'); setError('Authentication required. Please log in again.'); setLoading(false); // Fall back to mock data if no token const mockData = getMockPosts(); setPosts(mockData); setTotalPosts(mockData.length); setHasMore(false); return; } // Make API request const response = await fetch(`http://localhost:4000/api/posts?${params.toString()}`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } }); if (response.status === 401) { console.error('Authentication failed: Unauthorized'); setError('Your session has expired. Please log in again.'); // Fall back to mock data on auth error const mockData = getMockPosts(); setPosts(mockData); setTotalPosts(mockData.length); setHasMore(false); setLoading(false); return; } if (!response.ok) { throw new Error(`API request failed with status: ${response.status}`); } const data = await response.json(); if (data && Array.isArray(data.posts)) { // Transform API posts to frontend format const frontendPosts: FrontendPost[] = data.posts.map((apiPost: ApiPost) => ({ id: apiPost.post_id, title: apiPost.title || 'Untitled Post', description: apiPost.description || '', author: apiPost.influencer?.name || 'Unknown Author', authorType: 'influencer', // Default to influencer platform: apiPost.platform as FrontendPost['platform'], // Cast to expected platform type contentType: determineContentType(apiPost), timestamp: apiPost.published_at, engagement: { views: apiPost.views_count, likes: apiPost.likes_count, comments: apiPost.comments_count, shares: apiPost.shares_count }, url: apiPost.post_url })); setPosts(frontendPosts); setTotalPosts(data.total || frontendPosts.length); setOffset(frontendPosts.length); setHasMore(frontendPosts.length === pageSize); } else { console.warn('Invalid data format received from API, using mock data'); // Fall back to mock data if API response format is unexpected const mockData = getMockPosts(); setPosts(mockData); setTotalPosts(mockData.length); setHasMore(false); } } catch (err) { console.error('Error fetching posts:', err); setError('Failed to fetch posts. Using mock data instead.'); // Fall back to mock data on error const mockData = getMockPosts(); setPosts(mockData); setTotalPosts(mockData.length); setHasMore(false); } finally { setLoading(false); } }; // Filter posts based on selected filters const filteredPosts = posts.filter((post) => { if (platformFilter !== 'all' && post.platform !== platformFilter) { return false; } if (contentTypeFilter !== 'all' && post.contentType !== contentTypeFilter) { return false; } return true; }); // Get platform icon const getPlatformIcon = (platform: string) => { switch (platform) { case 'youtube': return ; case 'instagram': return ; case 'facebook': return ; case 'linkedin': return ; default: return ; } }; // Get content type badge const getContentTypeBadge = (contentType?: string) => { if (!contentType) return null; switch (contentType) { case 'post': return Post; case 'video': return Video; case 'reel': return Reel; case 'short': return Short; default: return null; } }; // Get author type badge const getAuthorTypeBadge = (authorType: string) => { switch (authorType) { case 'influencer': return } />; case 'brand': return } />; case 'official': return } />; default: return null; } }; // Get platform name const getPlatformName = (platform: string) => { switch (platform) { case 'youtube': return 'YouTube'; case 'instagram': return 'Instagram'; case 'facebook': return 'Facebook'; case 'linkedin': return 'LinkedIn'; case 'tiktok': return 'TikTok'; default: return platform; } }; // Handle post click to view comments const handleViewComments = (postId: string) => { navigate(`/comments?post_id=${postId}`); }; // 根据帖子信息确定内容类型 const determineContentType = (apiPost: ApiPost): 'post' | 'video' | 'reel' | 'short' => { const platform = apiPost.platform?.toLowerCase(); const url = apiPost.post_url?.toLowerCase() || ''; if (platform === 'youtube') { if (url.includes('/shorts/')) { return 'short'; } return 'video'; } if (platform === 'instagram') { if (url.includes('/reel/')) { return 'reel'; } return 'post'; } if (platform === 'tiktok') { return 'short'; } return 'post'; }; // Helper function to get mock posts (extracted from useMockData) const getMockPosts = (): FrontendPost[] => { return [ { id: '1', title: 'How to Get Started with React', description: 'A beginner-friendly guide to React development', author: 'Jane Smith', authorType: 'influencer', platform: 'youtube', contentType: 'video', timestamp: '2023-05-15T14:30:00Z', engagement: { views: 15000, likes: 1200, comments: 85, shares: 320 }, url: 'https://youtube.com/watch?v=abc123' }, // ... add more mock posts as needed ]; }; // 加载更多数据的函数 const loadMorePosts = useCallback(async () => { if (isLoadingMore || !hasMore) return; console.log('Loading more posts from offset:', offset); setIsLoadingMore(true); try { // 构建查询参数 const params = new URLSearchParams(); const pageSize = 20; params.append('limit', pageSize.toString()); params.append('offset', offset.toString()); params.append('sort', 'published_at'); params.append('order', 'desc'); if (platformFilter && platformFilter !== 'all') { params.append('platform', platformFilter); } if (influencerId) { params.append('influencer_id', influencerId); } if (projectId) { params.append('project_id', projectId); } // 获取认证token const token = await getAuthToken(); if (!token) { console.error('Authentication token not found'); setError('Authentication required. Please log in again.'); setIsLoadingMore(false); return; } console.log('Fetching more posts with params:', params.toString()); // 发送API请求 const response = await fetch(`http://localhost:4000/api/posts?${params.toString()}`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } }); if (response.status === 401) { console.error('Authentication failed: Unauthorized'); setError('Your session has expired. Please log in again.'); setIsLoadingMore(false); return; } if (!response.ok) { throw new Error(`API request failed with status: ${response.status}`); } const data = await response.json(); console.log('Received more posts:', data?.posts?.length || 0); if (data && Array.isArray(data.posts)) { // 转换API返回的数据为前端需要的格式 const newPosts: FrontendPost[] = data.posts.map((apiPost: ApiPost) => ({ id: apiPost.post_id, title: apiPost.title || 'Untitled Post', description: apiPost.description || '', author: apiPost.influencer?.name || 'Unknown Author', authorType: 'influencer', // 默认为influencer类型 platform: apiPost.platform as FrontendPost['platform'], contentType: determineContentType(apiPost), timestamp: apiPost.published_at, engagement: { views: apiPost.views_count, likes: apiPost.likes_count, comments: apiPost.comments_count, shares: apiPost.shares_count }, url: apiPost.post_url })); // 更新状态 setPosts(prevPosts => [...prevPosts, ...newPosts]); setTotalPosts(data.total || totalPosts); setOffset(prevOffset => prevOffset + newPosts.length); // 检查是否还有更多数据 setHasMore(newPosts.length === pageSize); console.log('Updated posts count:', posts.length + newPosts.length); } else { // 如果没有更多数据 console.log('No more posts available'); setHasMore(false); } } catch (err) { console.error('Error fetching more posts:', err); setError('Failed to fetch more posts.'); } finally { setIsLoadingMore(false); } }, [offset, platformFilter, influencerId, projectId, isLoadingMore, hasMore, totalPosts, posts.length]); // 设置Intersection Observer来监测滚动 useEffect(() => { if (loading) return; if (observer.current) { observer.current.disconnect(); } observer.current = new IntersectionObserver(entries => { if (entries[0].isIntersecting && hasMore && !isLoadingMore) { console.log('Intersection observed, loading more posts...'); loadMorePosts(); } }, { rootMargin: '100px', // 提前100px触发加载 threshold: 0.1 // 降低阈值,使其更容易触发 }); if (loadMoreRef.current) { observer.current.observe(loadMoreRef.current); } return () => { if (observer.current) { observer.current.disconnect(); } }; }, [loading, hasMore, isLoadingMore, loadMorePosts]); // 在组件加载时获取数据 useEffect(() => { console.log('Fetching initial posts data...'); fetchPosts(); // 组件卸载时清理 return () => { if (observer.current) { observer.current.disconnect(); } }; }, [influencerId, projectId, platformFilter]); // 获取Supabase会话和token的函数 const getAuthToken = async () => { try { // 尝试从localStorage中查找Supabase token // Supabase通常将token存储在以sb-开头的键中 const keys = Object.keys(localStorage); const supabaseTokenKey = keys.find(key => key.startsWith('sb-') && localStorage.getItem(key)?.includes('access_token')); if (supabaseTokenKey) { try { const supabaseData = JSON.parse(localStorage.getItem(supabaseTokenKey) || '{}'); return supabaseData.access_token; } catch (e) { console.error('Error parsing Supabase token:', e); } } // 如果没有找到Supabase token,尝试使用常规token return localStorage.getItem('token'); } catch (error) { console.error('Error getting auth token:', error); return null; } }; if (error) { return
{error}
; } return (
} loading={loading} className="mb-4" > ({ onClick: () => handleViewComments(post.id), style: { cursor: 'pointer' } })} columns={[ { title: 'Post', dataIndex: 'title', key: 'title', render: (_: unknown, post: FrontendPost) => (
{getPlatformIcon(post.platform)}
{post.title} {' '} {getContentTypeBadge(post.contentType)}
{post.description?.length > 80 ? `${post.description.substring(0, 80)}...` : post.description}
{format(new Date(post.timestamp), 'MMM d, yyyy')} {post.url && ( e.stopPropagation()} // Prevent row click when clicking the link > View Post )}
), }, { title: 'Author', dataIndex: 'author', key: 'author', render: (_: unknown, post: FrontendPost) => ( } /> {post.author} {getAuthorTypeBadge(post.authorType)} ), }, { title: 'Platform', dataIndex: 'platform', key: 'platform', render: (platform: string) => getPlatformName(platform), }, { title: 'Engagement', key: 'engagement', render: (_: unknown, post: FrontendPost) => ( {post.engagement.views !== undefined && ( Views: {post.engagement.views.toLocaleString()} )} {post.engagement.likes !== undefined && ( Likes: {post.engagement.likes.toLocaleString()} )} {post.engagement.comments !== undefined && ( Comments: {post.engagement.comments.toLocaleString()} )} {post.engagement.shares !== undefined && ( Shares: {post.engagement.shares.toLocaleString()} )} ), }, { title: 'Actions', key: 'actions', render: (_: unknown, post: FrontendPost) => ( ), }, ]} scroll={{ x: 'max-content' }} /> {/* 无限滚动加载指示器 */}
{isLoadingMore && (
)} {!hasMore && posts.length > 0 && (
No more posts to load
)} {!hasMore && posts.length === 0 && !loading && ( )}
{/* Filters Drawer */} setShowFilters(false)} open={showFilters} width={300} >
{/* Post Detail Drawer */} setSelectedPost(null)} open={!!selectedPost} width={500} > {selectedPost && (
{selectedPost.title} {getPlatformIcon(selectedPost.platform)} {getPlatformName(selectedPost.platform)} {getContentTypeBadge(selectedPost.contentType)}
{selectedPost.description}
Author: {selectedPost.author} {getAuthorTypeBadge(selectedPost.authorType)}
Published: {format(new Date(selectedPost.timestamp), 'PPP')}
{selectedPost.engagement.views !== undefined && (
Views: {selectedPost.engagement.views.toLocaleString()}
)} {selectedPost.engagement.likes !== undefined && (
Likes: {selectedPost.engagement.likes.toLocaleString()}
)} {selectedPost.engagement.comments !== undefined && (
Comments: {selectedPost.engagement.comments.toLocaleString()}
)} {selectedPost.engagement.shares !== undefined && (
Shares: {selectedPost.engagement.shares.toLocaleString()}
)}
{selectedPost.url && (
)}
)}
); }; export default PostList;