795 lines
25 KiB
TypeScript
795 lines
25 KiB
TypeScript
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<PostListProps> = ({ influencerId, projectId }) => {
|
||
const navigate = useNavigate();
|
||
const [posts, setPosts] = useState<FrontendPost[]>([]);
|
||
const [loading, setLoading] = useState<boolean>(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [selectedPost, setSelectedPost] = useState<FrontendPost | null>(null);
|
||
const [platformFilter, setPlatformFilter] = useState<string>('all');
|
||
const [contentTypeFilter, setContentTypeFilter] = useState<string>('all');
|
||
const [showFilters, setShowFilters] = useState<boolean>(false);
|
||
const [totalPosts, setTotalPosts] = useState<number>(0);
|
||
const [offset, setOffset] = useState<number>(0);
|
||
const [hasMore, setHasMore] = useState<boolean>(true);
|
||
const [isLoadingMore, setIsLoadingMore] = useState<boolean>(false);
|
||
const observer = useRef<IntersectionObserver | null>(null);
|
||
const loadMoreRef = useRef<HTMLDivElement>(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 <YoutubeOutlined style={{ color: '#FF0000' }} />;
|
||
case 'instagram':
|
||
return <InstagramOutlined style={{ color: '#E1306C' }} />;
|
||
case 'facebook':
|
||
return <FacebookOutlined style={{ color: '#1877F2' }} />;
|
||
case 'linkedin':
|
||
return <LinkedinOutlined style={{ color: '#0A66C2' }} />;
|
||
default:
|
||
return <GlobalOutlined />;
|
||
}
|
||
};
|
||
|
||
// Get content type badge
|
||
const getContentTypeBadge = (contentType?: string) => {
|
||
if (!contentType) return null;
|
||
|
||
switch (contentType) {
|
||
case 'post':
|
||
return <Tag color="blue">Post</Tag>;
|
||
case 'video':
|
||
return <Tag color="green">Video</Tag>;
|
||
case 'reel':
|
||
return <Tag color="purple">Reel</Tag>;
|
||
case 'short':
|
||
return <Tag color="orange">Short</Tag>;
|
||
default:
|
||
return null;
|
||
}
|
||
};
|
||
|
||
// Get author type badge
|
||
const getAuthorTypeBadge = (authorType: string) => {
|
||
switch (authorType) {
|
||
case 'influencer':
|
||
return <Badge count={<UserOutlined style={{ color: '#1890ff' }} />} />;
|
||
case 'brand':
|
||
return <Badge count={<ShopOutlined style={{ color: '#52c41a' }} />} />;
|
||
case 'official':
|
||
return <Badge count={<VerifiedOutlined style={{ color: '#722ed1' }} />} />;
|
||
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 <div>{error}</div>;
|
||
}
|
||
|
||
return (
|
||
<div className="post-list-container">
|
||
<Card
|
||
title="Posts"
|
||
extra={
|
||
<Space>
|
||
<Button
|
||
icon={<FilterOutlined />}
|
||
onClick={() => setShowFilters(true)}
|
||
>
|
||
Filters
|
||
</Button>
|
||
</Space>
|
||
}
|
||
loading={loading}
|
||
className="mb-4"
|
||
>
|
||
<Table
|
||
dataSource={filteredPosts}
|
||
rowKey="id"
|
||
pagination={false}
|
||
onRow={(post) => ({
|
||
onClick: () => handleViewComments(post.id),
|
||
style: { cursor: 'pointer' }
|
||
})}
|
||
columns={[
|
||
{
|
||
title: 'Post',
|
||
dataIndex: 'title',
|
||
key: 'title',
|
||
render: (_: unknown, post: FrontendPost) => (
|
||
<div>
|
||
<Space align="start">
|
||
{getPlatformIcon(post.platform)}
|
||
<div>
|
||
<div>
|
||
<Text strong>{post.title}</Text> {' '}
|
||
{getContentTypeBadge(post.contentType)}
|
||
</div>
|
||
<div>
|
||
<Text type="secondary" ellipsis={{ tooltip: post.description }}>
|
||
{post.description?.length > 80
|
||
? `${post.description.substring(0, 80)}...`
|
||
: post.description}
|
||
</Text>
|
||
</div>
|
||
<div style={{ marginTop: 4 }}>
|
||
<Space size={16}>
|
||
<Text type="secondary">
|
||
{format(new Date(post.timestamp), 'MMM d, yyyy')}
|
||
</Text>
|
||
{post.url && (
|
||
<a
|
||
href={post.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
onClick={(e) => e.stopPropagation()} // Prevent row click when clicking the link
|
||
>
|
||
View Post
|
||
</a>
|
||
)}
|
||
</Space>
|
||
</div>
|
||
</div>
|
||
</Space>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: 'Author',
|
||
dataIndex: 'author',
|
||
key: 'author',
|
||
render: (_: unknown, post: FrontendPost) => (
|
||
<Space>
|
||
<Avatar icon={<UserOutlined />} />
|
||
<Text>{post.author}</Text>
|
||
{getAuthorTypeBadge(post.authorType)}
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: 'Platform',
|
||
dataIndex: 'platform',
|
||
key: 'platform',
|
||
render: (platform: string) => getPlatformName(platform),
|
||
},
|
||
{
|
||
title: 'Engagement',
|
||
key: 'engagement',
|
||
render: (_: unknown, post: FrontendPost) => (
|
||
<Space direction="vertical" size={0}>
|
||
{post.engagement.views !== undefined && (
|
||
<Text>Views: {post.engagement.views.toLocaleString()}</Text>
|
||
)}
|
||
{post.engagement.likes !== undefined && (
|
||
<Text>Likes: {post.engagement.likes.toLocaleString()}</Text>
|
||
)}
|
||
{post.engagement.comments !== undefined && (
|
||
<Text>Comments: {post.engagement.comments.toLocaleString()}</Text>
|
||
)}
|
||
{post.engagement.shares !== undefined && (
|
||
<Text>Shares: {post.engagement.shares.toLocaleString()}</Text>
|
||
)}
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: 'Actions',
|
||
key: 'actions',
|
||
render: (_: unknown, post: FrontendPost) => (
|
||
<Space>
|
||
<Button
|
||
type="link"
|
||
onClick={(e) => {
|
||
e.stopPropagation(); // Prevent row click
|
||
setSelectedPost(post);
|
||
}}
|
||
>
|
||
Details
|
||
</Button>
|
||
<Button
|
||
type="link"
|
||
icon={<MessageOutlined />}
|
||
onClick={(e) => {
|
||
e.stopPropagation(); // Prevent row click
|
||
handleViewComments(post.id);
|
||
}}
|
||
>
|
||
Comments
|
||
</Button>
|
||
</Space>
|
||
),
|
||
},
|
||
]}
|
||
scroll={{ x: 'max-content' }}
|
||
/>
|
||
|
||
{/* 无限滚动加载指示器 */}
|
||
<div
|
||
ref={loadMoreRef}
|
||
style={{
|
||
textAlign: 'center',
|
||
padding: '20px 0',
|
||
visibility: loading || !hasMore ? 'hidden' : 'visible'
|
||
}}
|
||
>
|
||
{isLoadingMore && (
|
||
<div className="py-4">
|
||
<Spin tip="Loading more posts..." />
|
||
</div>
|
||
)}
|
||
{!hasMore && posts.length > 0 && (
|
||
<div className="py-4 text-gray-500">
|
||
No more posts to load
|
||
</div>
|
||
)}
|
||
{!hasMore && posts.length === 0 && !loading && (
|
||
<Empty description="No posts found" />
|
||
)}
|
||
</div>
|
||
</Card>
|
||
|
||
{/* Filters Drawer */}
|
||
<Drawer
|
||
title="Filters"
|
||
placement="right"
|
||
onClose={() => setShowFilters(false)}
|
||
open={showFilters}
|
||
width={300}
|
||
>
|
||
<Form layout="vertical">
|
||
<Form.Item label="Platform">
|
||
<Select
|
||
value={platformFilter}
|
||
onChange={(value: string) => setPlatformFilter(value)}
|
||
style={{ width: '100%' }}
|
||
>
|
||
<Option value="all">All Platforms</Option>
|
||
<Option value="youtube">YouTube</Option>
|
||
<Option value="instagram">Instagram</Option>
|
||
<Option value="facebook">Facebook</Option>
|
||
<Option value="linkedin">LinkedIn</Option>
|
||
<Option value="tiktok">TikTok</Option>
|
||
</Select>
|
||
</Form.Item>
|
||
|
||
<Form.Item label="Content Type">
|
||
<Select
|
||
value={contentTypeFilter}
|
||
onChange={(value: string) => setContentTypeFilter(value)}
|
||
style={{ width: '100%' }}
|
||
>
|
||
<Option value="all">All Types</Option>
|
||
<Option value="post">Post</Option>
|
||
<Option value="video">Video</Option>
|
||
<Option value="reel">Reel</Option>
|
||
<Option value="short">Short</Option>
|
||
</Select>
|
||
</Form.Item>
|
||
</Form>
|
||
</Drawer>
|
||
|
||
{/* Post Detail Drawer */}
|
||
<Drawer
|
||
title="Post Details"
|
||
placement="right"
|
||
onClose={() => setSelectedPost(null)}
|
||
open={!!selectedPost}
|
||
width={500}
|
||
>
|
||
{selectedPost && (
|
||
<div>
|
||
<Title level={4}>{selectedPost.title}</Title>
|
||
<Space>
|
||
{getPlatformIcon(selectedPost.platform)}
|
||
<Text>{getPlatformName(selectedPost.platform)}</Text>
|
||
{getContentTypeBadge(selectedPost.contentType)}
|
||
</Space>
|
||
|
||
<div style={{ margin: '16px 0' }}>
|
||
<Text>{selectedPost.description}</Text>
|
||
</div>
|
||
|
||
<div style={{ margin: '16px 0' }}>
|
||
<Text strong>Author:</Text> {selectedPost.author} {getAuthorTypeBadge(selectedPost.authorType)}
|
||
</div>
|
||
|
||
<div style={{ margin: '16px 0' }}>
|
||
<Text strong>Published:</Text> {format(new Date(selectedPost.timestamp), 'PPP')}
|
||
</div>
|
||
|
||
<Card title="Engagement Statistics">
|
||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||
{selectedPost.engagement.views !== undefined && (
|
||
<div>
|
||
<Text strong>Views:</Text> {selectedPost.engagement.views.toLocaleString()}
|
||
</div>
|
||
)}
|
||
{selectedPost.engagement.likes !== undefined && (
|
||
<div>
|
||
<Text strong>Likes:</Text> {selectedPost.engagement.likes.toLocaleString()}
|
||
</div>
|
||
)}
|
||
{selectedPost.engagement.comments !== undefined && (
|
||
<div>
|
||
<Text strong>Comments:</Text> {selectedPost.engagement.comments.toLocaleString()}
|
||
</div>
|
||
)}
|
||
{selectedPost.engagement.shares !== undefined && (
|
||
<div>
|
||
<Text strong>Shares:</Text> {selectedPost.engagement.shares.toLocaleString()}
|
||
</div>
|
||
)}
|
||
</Space>
|
||
</Card>
|
||
|
||
{selectedPost.url && (
|
||
<div style={{ marginTop: 16 }}>
|
||
<Button type="primary" href={selectedPost.url} target="_blank">
|
||
View Original Post
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</Drawer>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default PostList;
|