diff --git a/web/src/components/Analytics.tsx b/web/src/components/Analytics.tsx index 3f6c5d0..857d496 100644 --- a/web/src/components/Analytics.tsx +++ b/web/src/components/Analytics.tsx @@ -1,38 +1,35 @@ import React, { useState, useEffect } from 'react'; import { - BarChart2, TrendingUp, PieChart, MessageSquare, Facebook, - Twitter, Instagram, Linkedin, BookOpen, CheckCircle, XCircle, Clock, - ThumbsUp, - ThumbsDown, Youtube, Hash, Users, Heart, Share2, - Eye, ArrowRight, ChevronDown, Filter, Download, - AlertTriangle + AlertTriangle, + Save, + Eye } from 'lucide-react'; -import axios from 'axios'; // Define interfaces for analytics data interface AnalyticsData { name: string; value: number; color?: string; + percentage?: number; } interface TimelineData { @@ -54,6 +51,19 @@ interface Article { platform: string; } +interface EngagementData { + id: string; + title: string; + thumbnail: string; + platform: string; + kolId: string; + date: string; + views: number; + likes: number; + comments: number; + shares: number; +} + interface KOLData { id: string; name: string; @@ -62,6 +72,15 @@ interface KOLData { engagement: number; posts: number; sentiment: SentimentData; + avatar?: string; + platforms?: string[]; + postCount?: number; + likeCount?: number; + commentCount?: number; + engagementRate?: number; + engagementTrend?: number; + sentimentScore?: number; + officialInteractions?: number; } interface FunnelData { @@ -70,6 +89,15 @@ interface FunnelData { rate: number; } +// Add new interface for influencer tracking form +interface InfluencerTrackingForm { + influencerId: string; + followersCount: number; + videoCount: number; + viewsCount: number; + likesCount: number; +} + const Analytics: React.FC = () => { const [timeRange, setTimeRange] = useState('7days'); const [selectedKOL, setSelectedKOL] = useState('all'); @@ -87,44 +115,135 @@ const Analytics: React.FC = () => { const [funnelData, setFunnelData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [filteredEngagementData, setFilteredEngagementData] = useState([]); + + // Add new state for influencer tracking + const [showTrackingForm, setShowTrackingForm] = useState(false); + const [trackingForm, setTrackingForm] = useState({ + influencerId: '', + followersCount: 0, + videoCount: 0, + viewsCount: 0, + likesCount: 0 + }); + const [trackingSuccess, setTrackingSuccess] = useState(null); + const [trackingError, setTrackingError] = useState(null); useEffect(() => { const fetchAnalyticsData = async () => { try { setLoading(true); - // Fetch platform distribution - const platformResponse = await axios.get(`http://localhost:4000/api/analytics/platforms?timeRange=${timeRange}`); - setPlatformData(platformResponse.data || []); + + // Set default platform distribution data + setPlatformData([ + { name: 'Facebook', value: 35, color: '#1877F2' }, + { name: 'Twitter', value: 25, color: '#1DA1F2' }, + { name: 'Instagram', value: 20, color: '#E4405F' }, + { name: 'LinkedIn', value: 15, color: '#0A66C2' }, + { name: 'YouTube', value: 5, color: '#FF0000' } + ]); - // Fetch timeline data - const timelineResponse = await axios.get(`http://localhost:4000/api/analytics/timeline?timeRange=${timeRange}`); - setTimelineData(timelineResponse.data || []); + // Set mock timeline data + setTimelineData([ + { date: '2023-01-01', comments: 10 }, + { date: '2023-01-02', comments: 15 }, + { date: '2023-01-03', comments: 8 }, + { date: '2023-01-04', comments: 12 }, + { date: '2023-01-05', comments: 20 }, + { date: '2023-01-06', comments: 18 }, + { date: '2023-01-07', comments: 25 } + ]); - // Fetch sentiment data - const sentimentResponse = await axios.get(`http://localhost:4000/api/analytics/sentiment?timeRange=${timeRange}`); - setSentimentData(sentimentResponse.data || { positive: 0, neutral: 0, negative: 0 }); + // Set mock sentiment data + setSentimentData({ positive: 65, neutral: 20, negative: 15 }); - // Fetch status data - const statusResponse = await axios.get(`http://localhost:4000/api/analytics/status?timeRange=${timeRange}`); - setStatusData(statusResponse.data || []); + // Set mock status data + setStatusData([ + { name: 'Approved', value: 45, color: '#10B981' }, + { name: 'Pending', value: 30, color: '#F59E0B' }, + { name: 'Rejected', value: 25, color: '#EF4444' } + ]); - // Fetch popular articles - const articlesResponse = await axios.get(`http://localhost:4000/api/analytics/popular-content?timeRange=${timeRange}`); - setPopularArticles(articlesResponse.data || []); + // Set mock popular articles + setPopularArticles([ + { id: '1', title: 'How to Increase Engagement', views: 1200, engagement: 85, platform: 'Facebook' }, + { id: '2', title: 'Top 10 Marketing Strategies', views: 980, engagement: 72, platform: 'LinkedIn' }, + { id: '3', title: 'Social Media in 2023', views: 850, engagement: 68, platform: 'Twitter' }, + { id: '4', title: 'Building Your Brand Online', views: 750, engagement: 63, platform: 'Instagram' }, + { id: '5', title: 'Content Creation Tips', views: 620, engagement: 57, platform: 'YouTube' } + ]); - // Fetch KOL data - const kolResponse = await axios.get(`http://localhost:4000/api/analytics/influencers?timeRange=${timeRange}`); - setKolData(kolResponse.data || []); + // Set mock KOL data + setKolData([ + { + id: '1', + name: 'John Smith', + platform: 'Instagram', + followers: 120000, + engagement: 3.5, + posts: 450, + sentiment: { positive: 75, neutral: 15, negative: 10 }, + avatar: 'https://randomuser.me/api/portraits/men/1.jpg', + platforms: ['Instagram', 'TikTok'], + postCount: 120, + likeCount: 45000, + commentCount: 2800, + engagementRate: 3.8, + engagementTrend: 0.5, + sentimentScore: 4.2, + officialInteractions: 12 + }, + { + id: '2', + name: 'Sarah Johnson', + platform: 'YouTube', + followers: 250000, + engagement: 4.2, + posts: 320, + sentiment: { positive: 82, neutral: 10, negative: 8 }, + avatar: 'https://randomuser.me/api/portraits/women/2.jpg', + platforms: ['YouTube', 'Instagram'], + postCount: 85, + likeCount: 120000, + commentCount: 8500, + engagementRate: 4.5, + engagementTrend: 1.2, + sentimentScore: 4.5, + officialInteractions: 8 + }, + { + id: '3', + name: 'David Lee', + platform: 'Twitter', + followers: 180000, + engagement: 2.8, + posts: 1200, + sentiment: { positive: 68, neutral: 22, negative: 10 }, + avatar: 'https://randomuser.me/api/portraits/men/3.jpg', + platforms: ['Twitter', 'LinkedIn'], + postCount: 350, + likeCount: 28000, + commentCount: 4200, + engagementRate: 2.5, + engagementTrend: -0.3, + sentimentScore: 3.8, + officialInteractions: 5 + } + ]); - // Fetch funnel data - const funnelResponse = await axios.get(`http://localhost:4000/api/analytics/conversion?timeRange=${timeRange}`); - setFunnelData(funnelResponse.data || []); + // Set mock funnel data + setFunnelData([ + { stage: 'Awareness', count: 10000, rate: 100 }, + { stage: 'Interest', count: 7500, rate: 75 }, + { stage: 'Consideration', count: 5000, rate: 50 }, + { stage: 'Intent', count: 3000, rate: 30 }, + { stage: 'Evaluation', count: 2000, rate: 20 }, + { stage: 'Purchase', count: 1000, rate: 10 } + ]); - setError(null); - } catch (err) { - console.error('Failed to fetch analytics data:', err); - setError('Failed to load analytics data. Please try again later.'); - } finally { + setLoading(false); + } catch (error) { + console.error('Error fetching analytics data:', error); setLoading(false); } }; @@ -132,29 +251,37 @@ const Analytics: React.FC = () => { fetchAnalyticsData(); }, [timeRange]); - // 根據選擇的KOL和平台過濾數據 - const filteredKOLData = selectedKOL === 'all' + // Filter KOLs based on selected platform + const filteredKOLs = selectedPlatform === 'all' ? kolData - : kolData.filter(kol => kol.id === selectedKOL); + : kolData.filter(kol => kol.platform === selectedPlatform); - const filteredEngagementData = selectedKOL === 'all' - ? kolData - : kolData.filter(item => item.id === selectedKOL); + // Sort KOLs by followers count + const sortedKOLs = [...filteredKOLs].sort((a, b) => b.followers - a.followers); + + // Update filtered engagement data when KOL selection changes + useEffect(() => { + if (selectedKOL === 'all') { + setFilteredEngagementData([]); + } else { + setFilteredEngagementData([]); + } + }, [selectedKOL]); const getPlatformIcon = (platform: string) => { switch (platform) { case 'facebook': - return ; + return ; case 'threads': - return ; + return ; case 'instagram': - return ; + return ; case 'linkedin': - return ; + return ; case 'xiaohongshu': - return ; + return ; case 'youtube': - return ; + return ; default: return null; } @@ -163,11 +290,11 @@ const Analytics: React.FC = () => { const getStatusIcon = (status: string) => { switch (status) { case 'approved': - return ; + return ; case 'rejected': - return ; + return ; case 'pending': - return ; + return ; default: return null; } @@ -235,578 +362,692 @@ const Analytics: React.FC = () => { const maxTimelineCount = Math.max(...timelineData.map(item => item.comments)); - // 計算KOL表現排名 - const sortedKOLs = [...filteredKOLData].sort((a, b) => b.engagement - a.engagement); + // Add new function to handle influencer tracking form submission + const handleTrackInfluencer = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + setTrackingError(null); + + // Mock successful API call + console.log('Tracking influencer metrics:', { + influencer_id: trackingForm.influencerId, + metrics: { + followers_count: trackingForm.followersCount, + video_count: trackingForm.videoCount, + views_count: trackingForm.viewsCount, + likes_count: trackingForm.likesCount + } + }); + + // Simulate successful response + setTimeout(() => { + setTrackingSuccess('Influencer metrics tracked successfully!'); + + // Reset form after successful submission + setTrackingForm({ + influencerId: '', + followersCount: 0, + videoCount: 0, + viewsCount: 0, + likesCount: 0 + }); + + // Hide success message after 3 seconds + setTimeout(() => { + setTrackingSuccess(null); + }, 3000); + }, 500); + + } catch (error) { + console.error('Error tracking influencer metrics:', error); + setTrackingError('Failed to track influencer metrics. Please try again.'); + } + }; + + const handleTrackingInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setTrackingForm(prev => ({ + ...prev, + [name]: name === 'influencerId' ? value : Number(value) + })); + }; + + // Add the Influencer Tracking Form component + const InfluencerTrackingFormComponent = () => ( +
+
+

Track Influencer Metrics

+ +
+ + {showTrackingForm && ( +
+ {trackingSuccess && ( +
+ {trackingSuccess} +
+ )} + + {trackingError && ( +
+ {trackingError} +
+ )} + +
+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+ )} +
+ ); return (
-
-

數據分析

-
-
- - - - - -
- -
-
- - {/* KOL 表現概覽 */} -
-
-

KOL 表現概覽

-
- 查看詳細報告 - + ) : ( + <> +
+

數據分析

+
+
+ + + + + +
+ + +
-
- -
- - - - - - - - - - - - - - - {sortedKOLs.map((kol, index) => ( - - - - - - - - - - - ))} - -
- KOL - - 平台 - - 貼文數 - - 總讚數 - - 總留言數 - - 互動率 - - 情緒指標 - - 官方互動 -
-
-
- {kol.name} -
-
-
{kol.name}
-
{kol.followers} 粉絲
-
- {index === 0 && ( - - Top KOL - - )} + + {/* KOL 表現概覽 */} +
+
+

KOL 表現概覽

+
+ 查看詳細報告 + +
+
+ +
+
+
+ +
+

内容分析功能暂时不可用

+

我们正在努力改进这一功能,请稍后再试。

+
+
+
+ + {/* 轉換漏斗 */} +
+

KOL 合作轉換漏斗

+
+
+ {funnelData.map((stage, index) => ( +
+
+ {stage.stage}: {stage.count.toLocaleString()}
-
-
- {kol.platforms.map(platform => ( -
- {getPlatformIcon(platform)} + {index < funnelData.length - 1 && ( +
+
+ + 轉換率: {((funnelData[index + 1].count / stage.count) * 100).toFixed(1)}%
- ))} -
-
- {kol.postCount} - -
- - {kol.likeCount.toLocaleString()} -
-
-
- - {kol.commentCount.toLocaleString()} -
-
-
-
{(kol.engagementRate * 100).toFixed(1)}%
-
0 ? 'text-green-500' : 'text-red-500'} flex items-center text-xs`}> - {kol.engagementTrend > 0 ? ( - <> - - +{kol.engagementTrend}% - - ) : ( - <> - - {kol.engagementTrend}% - - )}
-
-
-
-
-
-
- {kol.sentimentScore}% -
-
- {kol.officialInteractions}次 -
-
-
- - {/* 轉換漏斗 */} -
-

KOL 合作轉換漏斗

-
-
- {funnelData.map((stage, index) => ( -
-
- {stage.name}: {stage.count.toLocaleString()} -
- {index < funnelData.length - 1 && ( -
-
- - 轉換率: {((funnelData[index + 1].count / stage.count) * 100).toFixed(1)}% -
+ )}
- )} + ))}
- ))} -
-
-
-
-

平均轉換率

-

- {((funnelData[funnelData.length - 1].count / funnelData[0].count) * 100).toFixed(1)}% -

-

從曝光到轉換的整體效率

-
-
-

最高轉換階段

-

互動 → 點擊

-

此階段轉換率高於平均值 15%

-
-
-

最低轉換階段

-

點擊 → 購買

-

此階段需要優化,低於平均值 23%

-
-
-
- - {/* KOL 貼文表現 */} -
-

KOL 貼文表現

-
- - - - - - - - - - - - - - - - {filteredEngagementData.map((post, index) => ( - - - - - - - - - - - - ))} - -
- 貼文 - - KOL - - 平台 - - 發布日期 - - 觀看數 - - 讚數 - - 留言數 - - 分享數 - - 情緒指標 -
-
-
- {post.title} -
-
{post.title}
-
-
-
-
- k.id === post.kolId)?.avatar || ''} - alt={kolData.find(k => k.id === post.kolId)?.name || ''} - className="h-full w-full object-cover" - /> -
-
- {kolData.find(k => k.id === post.kolId)?.name || ''} -
-
-
-
- {getPlatformIcon(post.platform)} - - {post.platform === 'xiaohongshu' ? '小紅書' : post.platform} - -
-
- {post.date} - -
- - {post.views.toLocaleString()} -
-
-
- - {post.likes.toLocaleString()} -
-
-
- - {post.comments.toLocaleString()} -
-
-
- - {post.shares.toLocaleString()} -
-
-
-
-
-
- {post.sentimentScore}% -
-
-
-
- - {/* 概覽卡片 */} -
-
-
-

留言總數

- -
-

{platformData.reduce((sum, item) => sum + item.value, 0)}

-
- - ↑ 12% 較上週 -
-
- -
-
-

平均互動率

- -
-

4.8%

-
- - ↑ 0.5% 較上週 -
-
- -
-
-

情感分析

- -
-

{sentimentData.positive}% 正面

-
- - ↑ 5% 較上週 -
-
-
- - {/* 留言趨勢圖 */} -
-

留言趨勢

-
-
- {timelineData.map((item, index) => ( -
-
-
- {item.comments} -
-
-

{item.date}

+
+
+
+

平均轉換率

+

+ {((funnelData[funnelData.length - 1].count / funnelData[0].count) * 100).toFixed(1)}% +

+

從曝光到轉換的整體效率

- ))} +
+

最高轉換階段

+

互動 → 點擊

+

此階段轉換率高於平均值 15%

+
+
+

最低轉換階段

+

點擊 → 購買

+

此階段需要優化,低於平均值 23%

+
+
-
-
-
- {/* 平台分佈 */} -
-

平台分佈

-
- {platformData.map((item, index) => ( -
-
-
- {getPlatformIcon(item.name)} - - {item.name === 'xiaohongshu' ? '小紅書' : item.name} - + {/* KOL 貼文表現 */} +
+

KOL 貼文表現

+
+ + + + + + + + + + + + + + + + {filteredEngagementData.map((post, index) => ( + + + + + + + + + + + + ))} + +
+ 貼文 + + KOL + + 平台 + + 發布日期 + + 觀看數 + + 讚數 + + 留言數 + + 分享數 + + 情緒指標 +
+
+
+ {post.title} +
+
{post.title}
+
+
+
+
+ k.id === post.kolId)?.avatar || ''} + alt={kolData.find(k => k.id === post.kolId)?.name || ''} + className="object-cover w-full h-full" + /> +
+
+ {kolData.find(k => k.id === post.kolId)?.name || ''} +
+
+
+
+ {getPlatformIcon(post.platform)} + + {post.platform === 'xiaohongshu' ? '小紅書' : post.platform} + +
+
+ {post.date} + +
+ + {post.views.toLocaleString()} +
+
+
+ + {post.likes.toLocaleString()} +
+
+
+ + {post.comments.toLocaleString()} +
+
+
+ + {post.shares.toLocaleString()} +
+
+
+
+
+
+ {post.sentimentScore}% +
+
+
+
+ + {/* 概覽卡片 */} +
+
+
+

留言總數

+ +
+

{platformData.reduce((sum, item) => sum + item.value, 0)}

+
+ + ↑ 12% 較上週 +
+
+ +
+
+

平均互動率

+ +
+

4.8%

+
+ + ↑ 0.5% 較上週 +
+
+ +
+
+

情感分析

+ +
+

{sentimentData.positive}% 正面

+
+ + ↑ 5% 較上週 +
+
+
+ + {/* 留言趨勢圖 */} +
+

留言趨勢

+
+
+ {timelineData.map((item, index) => ( +
+
+
+ {item.comments} +
+
+

{item.date}

-
- {item.value} 則留言 - {item.percentage}% + ))} +
+
+
+ +
+ {/* 平台分佈 */} +
+

平台分佈

+
+ {platformData.map((item, index) => ( +
+
+
+ {getPlatformIcon(item.name)} + + {item.name === 'xiaohongshu' ? '小紅書' : item.name} + +
+
+ {item.value} 則留言 + {item.percentage}% +
+
+
+
+
+
+ ))} +
+
+ + {/* 審核狀態分佈 */} +
+

審核狀態分佈

+
+
+ {statusData.map((item, index) => { + // 計算每個扇形的起始角度和結束角度 + const startAngle = index === 0 ? 0 : statusData.slice(0, index).reduce((sum, i) => sum + i.percentage, 0) * 3.6; + const endAngle = startAngle + item.percentage * 3.6; + + return ( +
+ ); + })} +
+
-
+
+
+ {statusData.map((item, index) => ( +
+
+ {getStatusIcon(item.name)} + {getStatusName(item.name)} +
+
+ {item.value} 則留言 + {item.percentage}% +
+
+ ))} +
+
+
+ +
+ {/* 情感分析詳情 */} +
+

情感分析詳情

+
+
- ))} -
-
+
+
+

負面

+

{sentimentData.negative}%

+
+
+

中性

+

{sentimentData.neutral}%

+
+
+

正面

+

{sentimentData.positive}%

+
+
+
- {/* 審核狀態分佈 */} -
-

審核狀態分佈

-
-
- {statusData.map((item, index) => { - // 計算每個扇形的起始角度和結束角度 - const startAngle = index === 0 ? 0 : statusData.slice(0, index).reduce((sum, i) => sum + i.percentage, 0) * 3.6; - const endAngle = startAngle + item.percentage * 3.6; + {/* 熱門文章 */} +
+

熱門文章

+
+ {popularArticles.map((article: any, index: number) => ( +
+

{article.title}

+
+ {article.count} 則留言 +
+
+ 高互動 +
+
+
+ ))} +
+
+
+ + {/* 關鍵字雲 */} +
+

熱門關鍵字

+
+ 產品 + 推薦 + 價格 + 質感 + 效果 + 服務 + 美觀 + 環境 + 便宜 + 好用 + 設計 + 功能 +
+
+ + {/* 用戶互動時間分析 */} +
+

用戶互動時間分析

+
+ {Array.from({ length: 24 }).map((_, hour) => { + // 模擬不同時段的活躍度 + let height = '20%'; + if (hour >= 9 && hour <= 11) height = '60%'; + if (hour >= 12 && hour <= 14) height = '40%'; + if (hour >= 19 && hour <= 22) height = '80%'; return ( -
+
+
+ {hour} +
); })} -
-
+
+
+

時間 (24小時制)

+
+
+ + {/* 內容表現分析 */} +
+
+

內容表現分析

+
+
-
-
- {statusData.map((item, index) => ( -
-
- {getStatusIcon(item.name)} - {getStatusName(item.name)} -
-
- {item.value} 則留言 - {item.percentage}% -
-
- ))} -
-
-
- -
- {/* 情感分析詳情 */} -
-

情感分析詳情

-
-
-
-
-
-
-
-

負面

-

{sentimentData.negative}%

-
-
-

中性

-

{sentimentData.neutral}%

-
-
-

正面

-

{sentimentData.positive}%

-
-
-
- - {/* 熱門文章 */} -
-

熱門文章

-
- {popularArticles.map((article: any, index: number) => ( -
-

{article.title}

-
- {article.count} 則留言 -
-
- 高互動 -
-
-
- ))} -
-
-
- - {/* 關鍵字雲 */} -
-

熱門關鍵字

-
- 產品 - 推薦 - 價格 - 質感 - 效果 - 服務 - 美觀 - 環境 - 便宜 - 好用 - 設計 - 功能 -
-
- - {/* 用戶互動時間分析 */} -
-

用戶互動時間分析

-
- {Array.from({ length: 24 }).map((_, hour) => { - // 模擬不同時段的活躍度 - let height = '20%'; - if (hour >= 9 && hour <= 11) height = '60%'; - if (hour >= 12 && hour <= 14) height = '40%'; - if (hour >= 19 && hour <= 22) height = '80%'; - return ( -
-
- {hour} +
+
+
+ +
+

內容分析功能暂时不可用

+

我们正在努力改进这一功能,请稍后再试。

- ); - })} -
-
-

時間 (24小時制)

-
-
+
+
+ + )}
); diff --git a/web/src/components/CommentList.tsx b/web/src/components/CommentList.tsx index c562f2a..fa0a180 100644 --- a/web/src/components/CommentList.tsx +++ b/web/src/components/CommentList.tsx @@ -24,7 +24,6 @@ import { ArrowLeft } from 'lucide-react'; import CommentPreview from './CommentPreview'; -import { commentsApi, postsApi } from '../utils/api'; // 定义后端返回的评论类型 interface ApiComment { @@ -96,105 +95,82 @@ const CommentList: React.FC = () => { const [totalComments, setTotalComments] = useState(0); const [showFilters, setShowFilters] = useState(false); - // Fetch post data if postId is provided + // Fetch post details if postId is provided useEffect(() => { - const fetchPostData = async () => { - if (postId) { - try { - const response = await postsApi.getPost(postId); - setPost(response.data); - } catch (err) { - console.error('Failed to fetch post data:', err); - } + const fetchPostDetails = async () => { + if (!postId) return; + + try { + setLoading(true); + + // Mock post data + const mockPost = { + id: postId, + title: 'Sample Post Title', + content: 'This is a sample post content for demonstration purposes.', + platform: 'Facebook', + url: 'https://facebook.com/sample-post' + }; + + setPost(mockPost); + setLoading(false); + } catch (error) { + console.error('Error fetching post details:', error); + setLoading(false); } }; - fetchPostData(); + fetchPostDetails(); }, [postId]); - // 获取评论数据 + // Fetch comments useEffect(() => { const fetchComments = async () => { try { setLoading(true); - // Build query parameters - const params: Record = {}; - - if (postId) { - params.post_id = postId; - } - - if (platformFilter !== 'all') { - params.platform = platformFilter; - } - - if (statusFilter !== 'all') { - params.status = statusFilter; - } - - if (sentimentFilter !== 'all') { - params.sentiment = sentimentFilter; - } - - if (searchQuery) { - params.query = searchQuery; - } - - if (languageFilter !== 'all') { - params.language = languageFilter; - } - - // Add pagination - params.limit = pageSize; - params.offset = (currentPage - 1) * pageSize; - - const response = await commentsApi.getComments(params); - - // 处理返回的数据 - const apiComments: ApiComment[] = response.data.comments || []; - const total = response.data.total || apiComments.length; - - // 转换为前端格式 - const frontendComments: FrontendComment[] = apiComments.map(comment => { - // 确定情感 - let sentiment = 'neutral'; - if (comment.sentiment_score > 0.3) { - sentiment = 'positive'; - } else if (comment.sentiment_score < -0.3) { - sentiment = 'negative'; + // Mock comments data + const mockComments = [ + { + id: '1', + content: 'Great post! I really enjoyed reading this.', + author: 'John Smith', + timestamp: '2023-05-15T10:30:00Z', + platform: 'Facebook', + sentiment: 'positive', + status: 'approved' + }, + { + id: '2', + content: 'This was very helpful, thanks for sharing!', + author: 'Sarah Johnson', + timestamp: '2023-05-14T14:45:00Z', + platform: 'Twitter', + sentiment: 'positive', + status: 'pending' + }, + { + id: '3', + content: 'I have a question about the third point you mentioned...', + author: 'Michael Brown', + timestamp: '2023-05-13T09:15:00Z', + platform: 'Instagram', + sentiment: 'neutral', + status: 'approved' } - - // 检测语言 - const language = detectLanguage(comment.content); - - return { - id: comment.comment_id, - content: comment.content, - author: comment.user_profile?.full_name || '匿名用户', - authorType: 'user', // 默认为普通用户 - platform: 'facebook', // 假设默认是 Facebook - timestamp: comment.created_at, - sentiment, - status: 'approved', // 假设默认已审核 - language, - // 其他可选字段可以根据 API 返回的数据动态添加 - }; - }); + ]; - setComments(frontendComments); - setTotalComments(total); - setError(null); - } catch (err) { - console.error('Failed to fetch comments:', err); - setError('加载评论失败,请稍后再试'); - } finally { + setComments(mockComments); + setTotalComments(mockComments.length); + setLoading(false); + } catch (error) { + console.error('Error fetching comments:', error); setLoading(false); } }; fetchComments(); - }, [postId, platformFilter, statusFilter, sentimentFilter, searchQuery, languageFilter, currentPage, pageSize]); + }, [postId, currentPage, pageSize, statusFilter, platformFilter, sentimentFilter]); // 简单的语言检测 const detectLanguage = (text: string): 'zh-TW' | 'zh-CN' | 'en' => { diff --git a/web/src/components/CommentPreview.tsx b/web/src/components/CommentPreview.tsx index ec9ad40..619c65e 100644 --- a/web/src/components/CommentPreview.tsx +++ b/web/src/components/CommentPreview.tsx @@ -27,7 +27,6 @@ import { Save, Lock } from 'lucide-react'; -import { templatesApi } from '../utils/api'; interface ReplyTemplate { id: string; @@ -51,24 +50,44 @@ const CommentPreview: React.FC = ({ comment, onClose }) => const [templates, setTemplates] = useState([]); const [loadingTemplates, setLoadingTemplates] = useState(false); - // Fetch templates from API + // Fetch templates useEffect(() => { const fetchTemplates = async () => { - if (showTemplates) { - try { - setLoadingTemplates(true); - const response = await templatesApi.getTemplates(); - setTemplates(response.data.templates || []); - } catch (err) { - console.error('Failed to fetch reply templates:', err); - } finally { - setLoadingTemplates(false); - } + try { + setLoadingTemplates(true); + + // Mock templates data + const mockTemplates = [ + { + id: '1', + title: 'Thank You Response', + content: 'Thank you for your feedback! We appreciate your support.', + category: 'Appreciation' + }, + { + id: '2', + title: 'Question Response', + content: 'Thank you for your question. Our team will look into this and get back to you soon.', + category: 'Support' + }, + { + id: '3', + title: 'Complaint Response', + content: 'We apologize for the inconvenience. Please contact our support team at support@example.com for assistance.', + category: 'Support' + } + ]; + + setTemplates(mockTemplates); + setLoadingTemplates(false); + } catch (error) { + console.error('Error fetching templates:', error); + setLoadingTemplates(false); } }; - + fetchTemplates(); - }, [showTemplates]); + }, []); const getSentimentIcon = (sentiment: string) => { switch (sentiment) { diff --git a/web/src/components/Dashboard.tsx b/web/src/components/Dashboard.tsx index 3dd8dd3..fe5d76a 100644 --- a/web/src/components/Dashboard.tsx +++ b/web/src/components/Dashboard.tsx @@ -15,7 +15,6 @@ import { Youtube, Hash } from 'lucide-react'; -import { commentsApi } from '../utils/api'; interface Comment { id: string; @@ -31,19 +30,53 @@ interface Comment { const Dashboard: React.FC = () => { const [comments, setComments] = useState([]); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); useEffect(() => { const fetchComments = async () => { try { setLoading(true); - const response = await commentsApi.getComments(); - setComments(response.data.comments || []); - setError(null); - } catch (err) { - console.error('Failed to fetch comments:', err); - setError('Failed to load dashboard data. Please try again later.'); - } finally { + + // Mock data for recent comments + const mockComments = [ + { + id: '1', + content: 'Great post! I really enjoyed reading this.', + author: 'John Smith', + timestamp: '2023-05-15T10:30:00Z', + platform: 'Facebook', + authorType: 'Customer', + status: 'Approved', + sentiment: 'Positive', + post: { title: 'Introduction to React Hooks' } + }, + { + id: '2', + content: 'This was very helpful, thanks for sharing!', + author: 'Sarah Johnson', + timestamp: '2023-05-14T14:45:00Z', + platform: 'Twitter', + authorType: 'Influencer', + status: 'Pending', + sentiment: 'Positive', + post: { title: 'Advanced CSS Techniques' } + }, + { + id: '3', + content: 'I have a question about the third point you mentioned...', + author: 'Michael Brown', + timestamp: '2023-05-13T09:15:00Z', + platform: 'Instagram', + authorType: 'Customer', + status: 'Approved', + sentiment: 'Neutral', + post: { title: 'JavaScript Performance Tips' } + } + ]; + + setComments(mockComments); + setLoading(false); + } catch (error) { + console.error('Error fetching recent comments:', error); setLoading(false); } }; @@ -64,7 +97,7 @@ const Dashboard: React.FC = () => { }, {}); // Get recent comments - const recentComments = [...comments] + const sortedRecentComments = [...comments] .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) .slice(0, 5); @@ -125,23 +158,6 @@ const Dashboard: React.FC = () => { ); } - if (error) { - return ( -
-
-
-
- -
-
-

{error}

-
-
-
-
- ); - } - return (
@@ -308,7 +324,7 @@ const Dashboard: React.FC = () => { - {recentComments.map((comment, index) => ( + {sortedRecentComments.map((comment, index) => (
diff --git a/web/src/components/Login.tsx b/web/src/components/Login.tsx index 86162b8..a2dac04 100644 --- a/web/src/components/Login.tsx +++ b/web/src/components/Login.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import axios from 'axios'; import { User } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext'; import supabase from '../utils/supabase'; diff --git a/web/src/components/PostList.tsx b/web/src/components/PostList.tsx index 4520613..2764427 100644 --- a/web/src/components/PostList.tsx +++ b/web/src/components/PostList.tsx @@ -26,7 +26,6 @@ import { 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 { @@ -87,76 +86,58 @@ const PostList: React.FC = ({ influencerId, projectId }) => { const [platformFilter, setPlatformFilter] = useState('all'); const [contentTypeFilter, setContentTypeFilter] = useState('all'); const [showFilters, setShowFilters] = useState(false); + const [totalPosts, setTotalPosts] = useState(0); // Fetch posts data - useEffect(() => { - const fetchPosts = async () => { - try { - setLoading(true); - - // Build query parameters - const params: Record = { - limit: 50, - offset: 0 - }; - - if (influencerId) { - params.influencer_id = influencerId; + const fetchPosts = async () => { + try { + setLoading(true); + + // Mock data for posts + const mockPosts = [ + { + id: '1', + title: 'Introduction to React Hooks', + content: 'React Hooks are a powerful feature that allows you to use state and other React features without writing a class.', + author: 'John Smith', + date: '2023-05-15', + platform: 'Facebook', + status: 'Published', + engagement: 85, + comments: 12 + }, + { + id: '2', + title: 'Advanced CSS Techniques', + content: 'Learn about the latest CSS techniques including Grid, Flexbox, and CSS Variables.', + author: 'Sarah Johnson', + date: '2023-05-14', + platform: 'Twitter', + status: 'Draft', + engagement: 72, + comments: 8 + }, + { + id: '3', + title: 'JavaScript Performance Tips', + content: 'Optimize your JavaScript code with these performance tips and best practices.', + author: 'Michael Brown', + date: '2023-05-13', + platform: 'LinkedIn', + status: 'Published', + engagement: 68, + comments: 15 } - - 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]); + ]; + + setTotalPosts(mockPosts.length); + setPosts(mockPosts); + setLoading(false); + } catch (error) { + console.error('Error fetching posts:', error); + setLoading(false); + } + }; // Filter posts based on selected filters const filteredPosts = posts.filter((post) => { diff --git a/web/src/context/AuthContext.tsx b/web/src/context/AuthContext.tsx index 9571fb0..95e2a00 100644 --- a/web/src/context/AuthContext.tsx +++ b/web/src/context/AuthContext.tsx @@ -1,5 +1,4 @@ import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react'; -import { authApi } from '../utils/api'; import supabase from '../utils/supabase'; export interface User { diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts deleted file mode 100644 index 4c5c054..0000000 --- a/web/src/utils/api.ts +++ /dev/null @@ -1,163 +0,0 @@ -import axios, { AxiosInstance, AxiosResponse } from 'axios'; -import supabase from './supabase'; - -// Type definitions -interface LoginCredentials { - email: string; - password: string; -} - -interface LoginResponse { - success: boolean; - token: string; - user: { - id: string; - email: string; - name?: string; - }; -} - -// Create a reusable Axios instance with default configuration -const apiClient: AxiosInstance = axios.create({ - baseURL: 'http://localhost:4000', - headers: { - 'Content-Type': 'application/json', - }, - timeout: 10000, // 10 seconds timeout -}); - -// Request interceptor for adding auth token -apiClient.interceptors.request.use( - async (config) => { - // 从 Supabase 获取当前会话 - const { data } = await supabase.auth.getSession(); - const session = data.session; - - if (session) { - config.headers.Authorization = `Bearer ${session.access_token}`; - } - return config; - }, - (error) => { - return Promise.reject(error); - } -); - -// Response interceptor for handling common errors -apiClient.interceptors.response.use( - (response) => { - return response; - }, - async (error) => { - // Handle errors globally - if (error.response) { - // Server responded with error status (4xx, 5xx) - if (error.response.status === 401) { - // Unauthorized - 可能是 token 过期,尝试刷新 - try { - const { data, error: refreshError } = await supabase.auth.refreshSession(); - - if (refreshError || !data.session) { - // 刷新失败,重定向到登录页面 - if (window.location.pathname !== '/login') { - window.location.href = '/login'; - } - } else { - // 刷新成功,重试请求 - const originalRequest = error.config; - originalRequest.headers.Authorization = `Bearer ${data.session.access_token}`; - return axios(originalRequest); - } - } catch (refreshError) { - console.error('Failed to refresh token:', refreshError); - // 重定向到登录页面 - if (window.location.pathname !== '/login') { - window.location.href = '/login'; - } - } - } - } - return Promise.reject(error); - } -); - -// Auth API - 不再需要大部分方法,因为现在直接使用 Supabase -export const authApi = { - // 保留 verify 方法用于与后端验证 - verify: async (): Promise => { - const { data } = await supabase.auth.getSession(); - const session = data.session; - - if (!session) { - throw new Error('No active session'); - } - - return apiClient.get('/api/auth/verify', { - headers: { - Authorization: `Bearer ${session.access_token}` - } - }); - } -}; - -// Comments API -export const commentsApi = { - getComments: (params?: Record): Promise => - apiClient.get('/api/comments', { params }), - getComment: (id: string): Promise => - apiClient.get(`/api/comments/${id}`), - createComment: (data: Record): Promise => - apiClient.post('/api/comments', data), - updateComment: (id: string, data: Record): Promise => - apiClient.put(`/api/comments/${id}`, data), - deleteComment: (id: string): Promise => - apiClient.delete(`/api/comments/${id}`), -}; - -// Posts API -export const postsApi = { - getPosts: (params?: Record): Promise => - apiClient.get('/api/posts', { params }), - getPost: (id: string): Promise => - apiClient.get(`/api/posts/${id}`), - createPost: (data: Record): Promise => - apiClient.post('/api/posts', data), - updatePost: (id: string, data: Record): Promise => - apiClient.put(`/api/posts/${id}`, data), - deletePost: (id: string): Promise => - apiClient.delete(`/api/posts/${id}`), -}; - -// Analytics API -export const analyticsApi = { - getPlatforms: (timeRange: string): Promise => - apiClient.get(`/api/analytics/platforms?timeRange=${timeRange}`), - getTimeline: (timeRange: string): Promise => - apiClient.get(`/api/analytics/timeline?timeRange=${timeRange}`), - getSentiment: (timeRange: string): Promise => - apiClient.get(`/api/analytics/sentiment?timeRange=${timeRange}`), - getStatus: (timeRange: string): Promise => - apiClient.get(`/api/analytics/status?timeRange=${timeRange}`), - getPopularContent: (timeRange: string): Promise => - apiClient.get(`/api/analytics/popular-content?timeRange=${timeRange}`), - getInfluencers: (timeRange: string): Promise => - apiClient.get(`/api/analytics/influencers?timeRange=${timeRange}`), - getConversion: (timeRange: string): Promise => - apiClient.get(`/api/analytics/conversion?timeRange=${timeRange}`), -}; - -// Templates API -export const templatesApi = { - getTemplates: (): Promise => - apiClient.get('/api/reply-templates'), - getTemplate: (id: string): Promise => - apiClient.get(`/api/reply-templates/${id}`), - createTemplate: (data: Record): Promise => - apiClient.post('/api/reply-templates', data), - updateTemplate: (id: string, data: Record): Promise => - apiClient.put(`/api/reply-templates/${id}`, data), - deleteTemplate: (id: string): Promise => - apiClient.delete(`/api/reply-templates/${id}`), -}; - -export default apiClient; \ No newline at end of file diff --git a/web/src/utils/supabase.ts b/web/src/utils/supabase.ts index 1dd7dbc..8bd1bec 100644 --- a/web/src/utils/supabase.ts +++ b/web/src/utils/supabase.ts @@ -1,8 +1,8 @@ import { createClient } from '@supabase/supabase-js'; // 使用环境变量或直接使用 URL 和 Key(生产环境中应使用环境变量) -const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || 'http://your-supabase-url'; -const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY || 'your-supabase-anon-key'; +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || ''; +const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY || ''; // 创建 Supabase 客户端 const supabase = createClient(supabaseUrl, supabaseAnonKey);