This commit is contained in:
2025-03-07 17:45:17 +08:00
commit 936af0c4ec
114 changed files with 37662 additions and 0 deletions

View File

@@ -0,0 +1,493 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Card,
Badge,
Table,
Avatar,
Button,
Space,
Tag,
Typography,
Select,
Drawer,
Form
} from 'antd';
import {
UserOutlined,
VerifiedOutlined,
ShopOutlined,
FilterOutlined,
YoutubeOutlined,
InstagramOutlined,
LinkedinOutlined,
FacebookOutlined,
GlobalOutlined,
MessageOutlined
} from '@ant-design/icons';
import { format } from 'date-fns';
import { postsApi } from '../utils/api';
// 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);
// Fetch posts data
useEffect(() => {
const fetchPosts = async () => {
try {
setLoading(true);
// Build query parameters
const params: Record<string, string | number> = {
limit: 50,
offset: 0
};
if (influencerId) {
params.influencer_id = influencerId;
}
if (projectId) {
params.project_id = projectId;
}
const response = await postsApi.getPosts(params);
// Process returned data
const apiPosts: ApiPost[] = response.data.posts || [];
// Transform API posts to frontend format
const processedPosts: FrontendPost[] = apiPosts.map((apiPost) => {
// Determine content type based on post data
let contentType: FrontendPost['contentType'] = 'post';
if (apiPost.platform === 'youtube') {
contentType = 'video';
} else if (
apiPost.platform === 'instagram' &&
apiPost.post_url?.includes('/reels/')
) {
contentType = 'reel';
}
return {
id: apiPost.post_id,
title: apiPost.title || 'Untitled Post',
description: apiPost.description || '',
author: apiPost.influencer?.name || 'Unknown',
authorType: 'influencer',
platform: apiPost.platform as FrontendPost['platform'],
contentType,
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(processedPosts);
setError(null);
} catch (err) {
console.error('Failed to fetch posts:', err);
setError('Failed to load posts. Please try again later.');
} finally {
setLoading(false);
}
};
fetchPosts();
}, [influencerId, projectId]);
// 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}`);
};
if (error) {
return <div>{error}</div>;
}
return (
<div>
<Card
title="Posts"
extra={
<Space>
<Button
icon={<FilterOutlined />}
onClick={() => setShowFilters(true)}
>
Filters
</Button>
</Space>
}
loading={loading}
>
<Table
dataSource={filteredPosts}
rowKey="id"
pagination={{ pageSize: 10 }}
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>
),
},
]}
/>
</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;