post comments fix
This commit is contained in:
@@ -595,16 +595,12 @@ const Analytics: React.FC = () => {
|
|||||||
|
|
||||||
// 项目选择器组件
|
// 项目选择器组件
|
||||||
const ProjectSelector = () => (
|
const ProjectSelector = () => (
|
||||||
<div className="mb-6">
|
<div className="relative w-64">
|
||||||
<label htmlFor="project-select" className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
选择项目
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<select
|
<select
|
||||||
id="project-select"
|
id="project-select"
|
||||||
value={selectedProject}
|
value={selectedProject}
|
||||||
onChange={(e) => setSelectedProject(e.target.value)}
|
onChange={(e) => setSelectedProject(e.target.value)}
|
||||||
className="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
|
className="block w-full py-2 pl-3 pr-10 text-base border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||||
>
|
>
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<option key={project.id} value={project.id}>
|
<option key={project.id} value={project.id}>
|
||||||
@@ -613,8 +609,7 @@ const Analytics: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
|
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
|
||||||
<ArrowRight className="h-4 w-4 text-gray-400" />
|
<ArrowRight className="w-4 h-4 text-gray-400" />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -624,11 +619,7 @@ const Analytics: React.FC = () => {
|
|||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h1 className="mb-6 text-2xl font-bold">Analytics Dashboard</h1>
|
<h1 className="mb-6 text-2xl font-bold">Analytics Dashboard</h1>
|
||||||
|
|
||||||
{/* 添加项目选择器 */}
|
{/* 删除这里的项目选择器 */}
|
||||||
<ProjectSelector />
|
|
||||||
|
|
||||||
{/* Add the Influencer Tracking Form at the top */}
|
|
||||||
<InfluencerTrackingFormComponent />
|
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex flex-col items-center justify-center h-80">
|
<div className="flex flex-col items-center justify-center h-80">
|
||||||
@@ -653,6 +644,7 @@ const Analytics: React.FC = () => {
|
|||||||
<div className="flex flex-col items-start justify-between mb-6 space-y-4 lg:flex-row lg:items-center lg:space-y-0">
|
<div className="flex flex-col items-start justify-between mb-6 space-y-4 lg:flex-row lg:items-center lg:space-y-0">
|
||||||
<h2 className="text-2xl font-bold text-gray-800">數據分析</h2>
|
<h2 className="text-2xl font-bold text-gray-800">數據分析</h2>
|
||||||
<div className="flex flex-col space-y-3 sm:flex-row sm:space-y-0 sm:space-x-4">
|
<div className="flex flex-col space-y-3 sm:flex-row sm:space-y-0 sm:space-x-4">
|
||||||
|
<ProjectSelector />
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<select
|
<select
|
||||||
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
|||||||
@@ -1,29 +1,25 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Facebook,
|
Facebook,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Instagram,
|
|
||||||
Linkedin,
|
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle,
|
XCircle,
|
||||||
MoreHorizontal,
|
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
BookOpen,
|
|
||||||
ThumbsUp,
|
ThumbsUp,
|
||||||
ThumbsDown,
|
ThumbsDown,
|
||||||
Minus,
|
Minus,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
User,
|
|
||||||
Award,
|
|
||||||
Briefcase,
|
|
||||||
Youtube,
|
|
||||||
Hash,
|
|
||||||
Filter,
|
Filter,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ArrowLeft
|
ArrowLeft,
|
||||||
|
Instagram,
|
||||||
|
Linkedin,
|
||||||
|
Youtube
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import CommentPreview from './CommentPreview';
|
import CommentPreview from './CommentPreview';
|
||||||
|
import { Spin, Empty } from 'antd';
|
||||||
|
import { Comment } from '../types';
|
||||||
|
|
||||||
// 定义后端返回的评论类型
|
// 定义后端返回的评论类型
|
||||||
interface ApiComment {
|
interface ApiComment {
|
||||||
@@ -39,26 +35,21 @@ interface ApiComment {
|
|||||||
full_name: string;
|
full_name: string;
|
||||||
avatar_url: string;
|
avatar_url: string;
|
||||||
};
|
};
|
||||||
|
post?: {
|
||||||
|
title: string;
|
||||||
|
post_id: string;
|
||||||
|
platform: string;
|
||||||
|
post_url: string;
|
||||||
|
description: string;
|
||||||
|
published_at: string;
|
||||||
|
influencer_id: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定义前端使用的评论类型
|
// 使用导入的Comment类型,不再需要FrontendComment接口
|
||||||
interface FrontendComment {
|
// interface FrontendComment {
|
||||||
id: string;
|
// ...
|
||||||
content: string;
|
// }
|
||||||
author: string;
|
|
||||||
authorType: 'user' | 'kol' | 'official';
|
|
||||||
platform: 'facebook' | 'threads' | 'instagram' | 'linkedin' | 'xiaohongshu' | 'youtube';
|
|
||||||
contentType?: 'post' | 'reel' | 'video' | 'short';
|
|
||||||
timestamp: string;
|
|
||||||
sentiment: string;
|
|
||||||
status: string;
|
|
||||||
replyStatus?: string;
|
|
||||||
language?: string;
|
|
||||||
articleTitle?: string;
|
|
||||||
postAuthor?: string;
|
|
||||||
postAuthorType?: string;
|
|
||||||
url?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CommentListProps {
|
interface CommentListProps {
|
||||||
postId?: string; // 可选的帖子 ID,如果提供则只获取该帖子的评论
|
postId?: string; // 可选的帖子 ID,如果提供则只获取该帖子的评论
|
||||||
@@ -70,6 +61,8 @@ interface PostData {
|
|||||||
description?: string;
|
description?: string;
|
||||||
platform: string;
|
platform: string;
|
||||||
post_url?: string;
|
post_url?: string;
|
||||||
|
author?: string;
|
||||||
|
authorType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommentList: React.FC<CommentListProps> = () => {
|
const CommentList: React.FC<CommentListProps> = () => {
|
||||||
@@ -77,23 +70,34 @@ const CommentList: React.FC<CommentListProps> = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const postId = searchParams.get('post_id');
|
const postId = searchParams.get('post_id');
|
||||||
|
|
||||||
const [comments, setComments] = useState<FrontendComment[]>([]);
|
const [comments, setComments] = useState<Comment[]>([]);
|
||||||
const [post, setPost] = useState<PostData | null>(null); // Store post data
|
const [post, setPost] = useState<PostData | null>(null); // Store post data
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [selectedComment, setSelectedComment] = useState<FrontendComment | null>(null);
|
const [selectedComment, setSelectedComment] = useState<Comment | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// 过滤和分页状态
|
// 过滤和分页状态
|
||||||
const [platformFilter, setPlatformFilter] = useState<string>('all');
|
const [platformFilter, setPlatformFilter] = useState<string>('all');
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||||
const [sentimentFilter, setSentimentFilter] = useState<string>('all');
|
const [sentimentFilter] = useState<string>('all');
|
||||||
const [replyStatusFilter, setReplyStatusFilter] = useState<string>('all');
|
const [replyStatusFilter, setReplyStatusFilter] = useState<string>('all');
|
||||||
const [languageFilter, setLanguageFilter] = useState<string>('all');
|
const [languageFilter, setLanguageFilter] = useState<string>('all');
|
||||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
const [offset, setOffset] = useState<number>(0);
|
||||||
const [pageSize, setPageSize] = useState<number>(10);
|
const [pageSize] = useState<number>(20);
|
||||||
const [totalComments, setTotalComments] = useState<number>(0);
|
const [totalComments, setTotalComments] = useState<number>(0);
|
||||||
const [showFilters, setShowFilters] = useState<boolean>(false);
|
const [showFilters, setShowFilters] = useState<boolean>(false);
|
||||||
|
const [hasMore, setHasMore] = useState<boolean>(true);
|
||||||
|
const [isLoadingMore, setIsLoadingMore] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// 无限滚动相关的引用
|
||||||
|
const observer = useRef<IntersectionObserver | null>(null);
|
||||||
|
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
// 添加日志,查看是否正确获取到postId
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('CommentList - postId from URL:', postId);
|
||||||
|
}, [postId]);
|
||||||
|
|
||||||
// Fetch post details if postId is provided
|
// Fetch post details if postId is provided
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -103,19 +107,95 @@ const CommentList: React.FC<CommentListProps> = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// Mock post data
|
// 获取认证token
|
||||||
const mockPost = {
|
const token = await getAuthToken();
|
||||||
id: postId,
|
|
||||||
title: 'Sample Post Title',
|
if (!token) {
|
||||||
content: 'This is a sample post content for demonstration purposes.',
|
console.error('Authentication token not found');
|
||||||
platform: 'Facebook',
|
setError('Authentication required. Please log in again.');
|
||||||
url: 'https://facebook.com/sample-post'
|
setLoading(false);
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从后端API获取帖子详情
|
||||||
|
const response = await fetch(`http://localhost:4000/api/posts/${postId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API request failed with status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
console.log('Post details from API:', data);
|
||||||
|
|
||||||
|
// 将API返回的平台值映射到Comment类型中定义的平台值
|
||||||
|
let mappedPlatform: Comment['platform'] = 'facebook'; // 默认值
|
||||||
|
const platformFromApi = data.platform || 'unknown';
|
||||||
|
|
||||||
|
// 映射平台值
|
||||||
|
switch(platformFromApi.toLowerCase()) {
|
||||||
|
case 'facebook':
|
||||||
|
mappedPlatform = 'facebook';
|
||||||
|
break;
|
||||||
|
case 'instagram':
|
||||||
|
mappedPlatform = 'instagram';
|
||||||
|
break;
|
||||||
|
case 'linkedin':
|
||||||
|
mappedPlatform = 'linkedin';
|
||||||
|
break;
|
||||||
|
case 'youtube':
|
||||||
|
mappedPlatform = 'youtube';
|
||||||
|
break;
|
||||||
|
case 'threads':
|
||||||
|
mappedPlatform = 'threads';
|
||||||
|
break;
|
||||||
|
case 'twitter': // 添加对Twitter的支持
|
||||||
|
mappedPlatform = 'twitter'; // 直接映射到twitter
|
||||||
|
break;
|
||||||
|
case 'xiaohongshu':
|
||||||
|
case '小红书':
|
||||||
|
mappedPlatform = 'xiaohongshu';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// 如果无法映射,使用默认值
|
||||||
|
console.warn(`Unknown platform: ${platformFromApi}, using default: facebook`);
|
||||||
|
mappedPlatform = 'facebook';
|
||||||
|
}
|
||||||
|
|
||||||
|
setPost({
|
||||||
|
id: data.post_id,
|
||||||
|
title: data.title || 'Untitled Post',
|
||||||
|
description: data.description || '',
|
||||||
|
platform: mappedPlatform, // 使用映射后的平台值
|
||||||
|
post_url: data.post_url,
|
||||||
|
author: data.influencer?.name || 'Unknown Author',
|
||||||
|
authorType: 'kol'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 如果找不到帖子,使用帖子ID作为标题
|
||||||
|
setPost({
|
||||||
|
id: postId,
|
||||||
|
title: `Post ${postId}`,
|
||||||
|
platform: 'facebook' // 使用有效的平台值
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setPost(mockPost);
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching post details:', error);
|
console.error('Error fetching post details:', error);
|
||||||
|
// 如果获取帖子详情失败,使用帖子ID作为标题
|
||||||
|
setPost({
|
||||||
|
id: postId,
|
||||||
|
title: `Post ${postId}`,
|
||||||
|
platform: 'facebook' // 使用有效的平台值
|
||||||
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -123,54 +203,394 @@ const CommentList: React.FC<CommentListProps> = () => {
|
|||||||
fetchPostDetails();
|
fetchPostDetails();
|
||||||
}, [postId]);
|
}, [postId]);
|
||||||
|
|
||||||
// Fetch comments
|
// 加载更多评论的函数
|
||||||
|
const loadMoreComments = useCallback(async () => {
|
||||||
|
if (isLoadingMore || !hasMore) return;
|
||||||
|
|
||||||
|
console.log('Loading more comments from offset:', offset);
|
||||||
|
setIsLoadingMore(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取认证token
|
||||||
|
const token = await getAuthToken();
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.error('Authentication token not found');
|
||||||
|
setError('Authentication required. Please log in again.');
|
||||||
|
setIsLoadingMore(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建查询参数
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
// 如果URL中有post_id参数,则只获取该帖子的评论
|
||||||
|
if (postId) {
|
||||||
|
console.log('Loading more comments for specific post:', postId);
|
||||||
|
params.append('post_id', postId);
|
||||||
|
}
|
||||||
|
|
||||||
|
params.append('limit', pageSize.toString());
|
||||||
|
params.append('offset', offset.toString());
|
||||||
|
|
||||||
|
if (platformFilter !== 'all') {
|
||||||
|
params.append('platform', platformFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusFilter !== 'all') {
|
||||||
|
params.append('status', statusFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sentimentFilter !== 'all') {
|
||||||
|
params.append('sentiment', sentimentFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replyStatusFilter !== 'all') {
|
||||||
|
params.append('reply_status', replyStatusFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery) {
|
||||||
|
params.append('search', searchQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Fetching more comments with params:', params.toString());
|
||||||
|
|
||||||
|
// 从后端API获取评论数据
|
||||||
|
const response = await fetch(`http://localhost:4000/api/comments?${params.toString()}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API request failed with status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Received more comments:', data?.comments?.length || 0);
|
||||||
|
|
||||||
|
if (data && Array.isArray(data.comments)) {
|
||||||
|
// 转换API返回的数据为前端需要的格式
|
||||||
|
const newComments: Comment[] = data.comments.map((apiComment: ApiComment) => {
|
||||||
|
// 计算情感分析结果
|
||||||
|
let sentiment: 'positive' | 'negative' | 'neutral' | 'mixed' = 'neutral';
|
||||||
|
if (apiComment.sentiment_score > 0.3) {
|
||||||
|
sentiment = 'positive';
|
||||||
|
} else if (apiComment.sentiment_score < -0.3) {
|
||||||
|
sentiment = 'negative';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测语言
|
||||||
|
const language = detectLanguage(apiComment.content);
|
||||||
|
|
||||||
|
// 使用API返回的post数据中的platform,如果没有则使用post对象中的platform
|
||||||
|
const platformFromApi = apiComment.post?.platform || (post?.platform || 'unknown');
|
||||||
|
console.log('Comment platform from API:', platformFromApi, 'Post platform:', post?.platform);
|
||||||
|
|
||||||
|
// 将API返回的平台值映射到Comment类型中定义的平台值
|
||||||
|
let mappedPlatform: Comment['platform'] = 'facebook'; // 默认值
|
||||||
|
|
||||||
|
// 映射平台值
|
||||||
|
switch(platformFromApi.toLowerCase()) {
|
||||||
|
case 'facebook':
|
||||||
|
mappedPlatform = 'facebook';
|
||||||
|
break;
|
||||||
|
case 'instagram':
|
||||||
|
mappedPlatform = 'instagram';
|
||||||
|
break;
|
||||||
|
case 'linkedin':
|
||||||
|
mappedPlatform = 'linkedin';
|
||||||
|
break;
|
||||||
|
case 'youtube':
|
||||||
|
mappedPlatform = 'youtube';
|
||||||
|
break;
|
||||||
|
case 'threads':
|
||||||
|
mappedPlatform = 'threads';
|
||||||
|
break;
|
||||||
|
case 'twitter': // 添加对Twitter的支持
|
||||||
|
mappedPlatform = 'twitter'; // 直接映射到twitter
|
||||||
|
break;
|
||||||
|
case 'xiaohongshu':
|
||||||
|
case '小红书':
|
||||||
|
mappedPlatform = 'xiaohongshu';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// 如果无法映射,使用默认值
|
||||||
|
console.warn(`Unknown platform: ${platformFromApi}, using default: facebook`);
|
||||||
|
mappedPlatform = 'facebook';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: apiComment.comment_id,
|
||||||
|
content: apiComment.content,
|
||||||
|
author: apiComment.user_profile?.full_name || 'Anonymous User',
|
||||||
|
authorType: 'user', // 默认为普通用户
|
||||||
|
platform: mappedPlatform, // 使用映射后的平台值
|
||||||
|
timestamp: apiComment.created_at,
|
||||||
|
sentiment: sentiment,
|
||||||
|
status: 'approved', // 默认状态
|
||||||
|
replyStatus: 'none',
|
||||||
|
language: language as Comment['language'],
|
||||||
|
articleTitle: apiComment.post?.title || 'Untitled Post',
|
||||||
|
postAuthor: post?.author || 'Unknown Author',
|
||||||
|
postAuthorType: 'kol', // 默认为KOL
|
||||||
|
url: apiComment.post?.post_url || '#'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
setComments(prevComments => [...prevComments, ...newComments]);
|
||||||
|
setTotalComments(data.count || totalComments);
|
||||||
|
setOffset(prevOffset => prevOffset + newComments.length);
|
||||||
|
|
||||||
|
// 检查是否还有更多数据
|
||||||
|
setHasMore(newComments.length === pageSize);
|
||||||
|
|
||||||
|
console.log('Updated comments count:', comments.length + newComments.length);
|
||||||
|
} else {
|
||||||
|
// 如果没有更多数据
|
||||||
|
console.log('No more comments available');
|
||||||
|
setHasMore(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching more comments:', err);
|
||||||
|
setError('Failed to fetch more comments.');
|
||||||
|
} finally {
|
||||||
|
setIsLoadingMore(false);
|
||||||
|
}
|
||||||
|
}, [offset, platformFilter, statusFilter, sentimentFilter, replyStatusFilter, searchQuery, postId, pageSize, isLoadingMore, hasMore, totalComments, comments.length, post]);
|
||||||
|
|
||||||
|
// 设置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 comments...');
|
||||||
|
loadMoreComments();
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
rootMargin: '100px', // 提前100px触发加载
|
||||||
|
threshold: 0.1 // 降低阈值,使其更容易触发
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loadMoreRef.current) {
|
||||||
|
observer.current.observe(loadMoreRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (observer.current) {
|
||||||
|
observer.current.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [loading, hasMore, isLoadingMore, loadMoreComments]);
|
||||||
|
|
||||||
|
// Fetch comments - 修改为初始加载
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchComments = async () => {
|
const fetchComments = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setComments([]);
|
||||||
|
setOffset(0);
|
||||||
|
setHasMore(true);
|
||||||
|
|
||||||
// Mock comments data
|
// 获取认证token
|
||||||
const mockComments = [
|
const token = await getAuthToken();
|
||||||
{
|
|
||||||
id: '1',
|
if (!token) {
|
||||||
content: 'Great post! I really enjoyed reading this.',
|
console.error('Authentication token not found');
|
||||||
author: 'John Smith',
|
setError('Authentication required. Please log in again.');
|
||||||
timestamp: '2023-05-15T10:30:00Z',
|
setLoading(false);
|
||||||
platform: 'Facebook',
|
return;
|
||||||
sentiment: 'positive',
|
}
|
||||||
status: 'approved'
|
|
||||||
},
|
// 构建查询参数
|
||||||
{
|
const params = new URLSearchParams();
|
||||||
id: '2',
|
|
||||||
content: 'This was very helpful, thanks for sharing!',
|
// 如果URL中有post_id参数,则只获取该帖子的评论
|
||||||
author: 'Sarah Johnson',
|
if (postId) {
|
||||||
timestamp: '2023-05-14T14:45:00Z',
|
console.log('Fetching comments for specific post:', postId);
|
||||||
platform: 'Twitter',
|
params.append('post_id', postId);
|
||||||
sentiment: 'positive',
|
} else {
|
||||||
status: 'pending'
|
console.log('Fetching all comments (no specific post)');
|
||||||
},
|
}
|
||||||
{
|
|
||||||
id: '3',
|
params.append('limit', pageSize.toString());
|
||||||
content: 'I have a question about the third point you mentioned...',
|
params.append('offset', '0');
|
||||||
author: 'Michael Brown',
|
|
||||||
timestamp: '2023-05-13T09:15:00Z',
|
if (platformFilter !== 'all') {
|
||||||
platform: 'Instagram',
|
params.append('platform', platformFilter);
|
||||||
sentiment: 'neutral',
|
}
|
||||||
status: 'approved'
|
|
||||||
|
if (statusFilter !== 'all') {
|
||||||
|
params.append('status', statusFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sentimentFilter !== 'all') {
|
||||||
|
params.append('sentiment', sentimentFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replyStatusFilter !== 'all') {
|
||||||
|
params.append('reply_status', replyStatusFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery) {
|
||||||
|
params.append('search', searchQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Fetching initial comments with params:', params.toString());
|
||||||
|
|
||||||
|
// 从后端API获取评论数据
|
||||||
|
const response = await fetch(`http://localhost:4000/api/comments?${params.toString()}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API request failed with status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data && Array.isArray(data.comments)) {
|
||||||
|
// 转换API返回的数据为前端需要的格式
|
||||||
|
const frontendComments: Comment[] = data.comments.map((apiComment: ApiComment) => {
|
||||||
|
// 计算情感分析结果
|
||||||
|
let sentiment: 'positive' | 'negative' | 'neutral' | 'mixed' = 'neutral';
|
||||||
|
if (apiComment.sentiment_score > 0.3) {
|
||||||
|
sentiment = 'positive';
|
||||||
|
} else if (apiComment.sentiment_score < -0.3) {
|
||||||
|
sentiment = 'negative';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测语言
|
||||||
|
const language = detectLanguage(apiComment.content);
|
||||||
|
|
||||||
|
// 使用API返回的post数据中的platform,如果没有则使用post对象中的platform
|
||||||
|
const platformFromApi = apiComment.post?.platform || (post?.platform || 'unknown');
|
||||||
|
console.log('Comment platform from API:', platformFromApi, 'Post platform:', post?.platform);
|
||||||
|
|
||||||
|
// 将API返回的平台值映射到Comment类型中定义的平台值
|
||||||
|
let mappedPlatform: Comment['platform'] = 'facebook'; // 默认值
|
||||||
|
|
||||||
|
// 映射平台值
|
||||||
|
switch(platformFromApi.toLowerCase()) {
|
||||||
|
case 'facebook':
|
||||||
|
mappedPlatform = 'facebook';
|
||||||
|
break;
|
||||||
|
case 'instagram':
|
||||||
|
mappedPlatform = 'instagram';
|
||||||
|
break;
|
||||||
|
case 'linkedin':
|
||||||
|
mappedPlatform = 'linkedin';
|
||||||
|
break;
|
||||||
|
case 'youtube':
|
||||||
|
mappedPlatform = 'youtube';
|
||||||
|
break;
|
||||||
|
case 'threads':
|
||||||
|
mappedPlatform = 'threads';
|
||||||
|
break;
|
||||||
|
case 'twitter': // 添加对Twitter的支持
|
||||||
|
mappedPlatform = 'twitter'; // 直接映射到twitter
|
||||||
|
break;
|
||||||
|
case 'xiaohongshu':
|
||||||
|
case '小红书':
|
||||||
|
mappedPlatform = 'xiaohongshu';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// 如果无法映射,使用默认值
|
||||||
|
console.warn(`Unknown platform: ${platformFromApi}, using default: facebook`);
|
||||||
|
mappedPlatform = 'facebook';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: apiComment.comment_id,
|
||||||
|
content: apiComment.content,
|
||||||
|
author: apiComment.user_profile?.full_name || 'Anonymous User',
|
||||||
|
authorType: 'user', // 默认为普通用户
|
||||||
|
platform: mappedPlatform, // 使用映射后的平台值
|
||||||
|
timestamp: apiComment.created_at,
|
||||||
|
sentiment: sentiment,
|
||||||
|
status: 'approved', // 默认状态
|
||||||
|
replyStatus: 'none',
|
||||||
|
language: language as Comment['language'],
|
||||||
|
articleTitle: apiComment.post?.title || 'Untitled Post',
|
||||||
|
postAuthor: post?.author || 'Unknown Author',
|
||||||
|
postAuthorType: 'kol', // 默认为KOL
|
||||||
|
url: apiComment.post?.post_url || '#'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setComments(frontendComments);
|
||||||
|
setTotalComments(data.count || frontendComments.length);
|
||||||
|
setOffset(frontendComments.length);
|
||||||
|
setHasMore(frontendComments.length === pageSize);
|
||||||
|
|
||||||
|
console.log('Initial comments loaded:', frontendComments.length);
|
||||||
|
} else {
|
||||||
|
// 如果没有评论数据
|
||||||
|
setComments([]);
|
||||||
|
setTotalComments(0);
|
||||||
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
];
|
|
||||||
|
|
||||||
setComments(mockComments);
|
|
||||||
setTotalComments(mockComments.length);
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching comments:', error);
|
console.error('Error fetching comments:', error);
|
||||||
|
setError('Failed to fetch comments. Please try again later.');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
|
// 如果获取评论失败,使用空数组
|
||||||
|
setComments([]);
|
||||||
|
setTotalComments(0);
|
||||||
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('Fetching initial comments data...');
|
||||||
fetchComments();
|
fetchComments();
|
||||||
}, [postId, currentPage, pageSize, statusFilter, platformFilter, sentimentFilter]);
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
return () => {
|
||||||
|
if (observer.current) {
|
||||||
|
observer.current.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [postId, pageSize, statusFilter, platformFilter, sentimentFilter, replyStatusFilter, searchQuery, post]);
|
||||||
|
|
||||||
|
// 获取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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 简单的语言检测
|
// 简单的语言检测
|
||||||
const detectLanguage = (text: string): 'zh-TW' | 'zh-CN' | 'en' => {
|
const detectLanguage = (text: string): 'zh-TW' | 'zh-CN' | 'en' => {
|
||||||
@@ -193,6 +613,62 @@ const CommentList: React.FC<CommentListProps> = () => {
|
|||||||
navigate('/posts');
|
navigate('/posts');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 根据平台类型返回相应的图标和名称
|
||||||
|
const getPlatformIcon = (platform: string) => {
|
||||||
|
// 确保platform是字符串并转换为小写
|
||||||
|
const platformLower = (platform || '').toLowerCase();
|
||||||
|
|
||||||
|
switch (platformLower) {
|
||||||
|
case 'facebook':
|
||||||
|
return {
|
||||||
|
icon: <Facebook className="w-5 h-5 text-blue-600" />,
|
||||||
|
name: 'Facebook'
|
||||||
|
};
|
||||||
|
case 'instagram':
|
||||||
|
return {
|
||||||
|
icon: <Instagram className="w-5 h-5 text-pink-600" />,
|
||||||
|
name: 'Instagram'
|
||||||
|
};
|
||||||
|
case 'linkedin':
|
||||||
|
return {
|
||||||
|
icon: <Linkedin className="w-5 h-5 text-blue-800" />,
|
||||||
|
name: 'LinkedIn'
|
||||||
|
};
|
||||||
|
case 'youtube':
|
||||||
|
return {
|
||||||
|
icon: <Youtube className="w-5 h-5 text-red-600" />,
|
||||||
|
name: 'YouTube'
|
||||||
|
};
|
||||||
|
case 'threads':
|
||||||
|
return {
|
||||||
|
icon: <MessageSquare className="w-5 h-5 text-black" />,
|
||||||
|
name: 'Threads'
|
||||||
|
};
|
||||||
|
case 'twitter': // 添加对Twitter的支持
|
||||||
|
return {
|
||||||
|
icon: <MessageSquare className="w-5 h-5 text-blue-400" />,
|
||||||
|
name: 'Twitter'
|
||||||
|
};
|
||||||
|
case 'xiaohongshu':
|
||||||
|
case '小红书':
|
||||||
|
return {
|
||||||
|
icon: <div className="flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full">小</div>,
|
||||||
|
name: '小红书'
|
||||||
|
};
|
||||||
|
case 'tiktok':
|
||||||
|
return {
|
||||||
|
icon: <div className="flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-black rounded-full">T</div>,
|
||||||
|
name: 'TikTok'
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
console.log('Unknown platform:', platform);
|
||||||
|
return {
|
||||||
|
icon: <MessageSquare className="w-5 h-5 text-gray-600" />,
|
||||||
|
name: platform || 'Unknown'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 显示加载状态
|
// 显示加载状态
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -235,24 +711,22 @@ const CommentList: React.FC<CommentListProps> = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
<div className="flex flex-col flex-1 overflow-hidden">
|
<div className="flex flex-col flex-1 overflow-hidden">
|
||||||
<div className="bg-white p-4 border-b flex items-center justify-between">
|
<div className="flex items-center justify-between p-4 bg-white border-b">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{postId && (
|
{postId && (
|
||||||
<button
|
<button
|
||||||
onClick={handleBackToPosts}
|
onClick={handleBackToPosts}
|
||||||
className="mr-4 p-1 hover:bg-gray-100 rounded-full transition-colors duration-200"
|
className="p-1 mr-4 transition-colors duration-200 rounded-full hover:bg-gray-100"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-5 w-5 text-gray-500" />
|
<ArrowLeft className="w-5 h-5 text-gray-500" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<h2 className="text-lg font-semibold">
|
<h2 className="text-lg font-semibold">
|
||||||
{post ? `${post.title} 的评论` : '所有评论'}
|
{postId ? (post ? `"${post.title}" 的评论` : '加载帖子中...') : '所有评论'}
|
||||||
</h2>
|
</h2>
|
||||||
{post && (
|
|
||||||
<span className="ml-2 text-sm text-gray-500">
|
<span className="ml-2 text-sm text-gray-500">
|
||||||
({totalComments} 条评论)
|
({totalComments} 条评论)
|
||||||
</span>
|
</span>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -260,7 +734,7 @@ const CommentList: React.FC<CommentListProps> = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="搜索评论..."
|
placeholder="搜索评论..."
|
||||||
className="px-4 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="px-4 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -271,9 +745,9 @@ const CommentList: React.FC<CommentListProps> = () => {
|
|||||||
showFilters ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100'
|
showFilters ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Filter className="h-4 w-4 mr-1" />
|
<Filter className="w-4 h-4 mr-1" />
|
||||||
筛选
|
筛选
|
||||||
<ChevronDown className="h-4 w-4 ml-1" />
|
<ChevronDown className="w-4 h-4 ml-1" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -342,7 +816,9 @@ const CommentList: React.FC<CommentListProps> = () => {
|
|||||||
{/* Mobile comment list */}
|
{/* Mobile comment list */}
|
||||||
<div className="block md:hidden">
|
<div className="block md:hidden">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{comments.map((comment) => (
|
{comments.map((comment) => {
|
||||||
|
const platformInfo = getPlatformIcon(comment.platform);
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={comment.id}
|
key={comment.id}
|
||||||
className="overflow-hidden bg-white rounded-lg shadow cursor-pointer"
|
className="overflow-hidden bg-white rounded-lg shadow cursor-pointer"
|
||||||
@@ -351,8 +827,8 @@ const CommentList: React.FC<CommentListProps> = () => {
|
|||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex items-start justify-between mb-3">
|
<div className="flex items-start justify-between mb-3">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Facebook className="w-5 h-5 text-blue-600" />
|
{platformInfo.icon}
|
||||||
<span className="ml-2 text-sm font-medium">Facebook</span>
|
<span className="ml-2 text-sm font-medium">{platformInfo.name}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-2 text-sm text-gray-900">{comment.content}</p>
|
<p className="mb-2 text-sm text-gray-900">{comment.content}</p>
|
||||||
@@ -364,7 +840,8 @@ const CommentList: React.FC<CommentListProps> = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -385,7 +862,9 @@ const CommentList: React.FC<CommentListProps> = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{comments.map((comment) => (
|
{comments.map((comment) => {
|
||||||
|
const platformInfo = getPlatformIcon(comment.platform);
|
||||||
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={comment.id}
|
key={comment.id}
|
||||||
className="cursor-pointer hover:bg-gray-50"
|
className="cursor-pointer hover:bg-gray-50"
|
||||||
@@ -394,9 +873,9 @@ const CommentList: React.FC<CommentListProps> = () => {
|
|||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Facebook className="w-5 h-5 text-blue-600" />
|
{platformInfo.icon}
|
||||||
<span className="ml-2 text-sm text-gray-900">
|
<span className="ml-2 text-sm text-gray-900">
|
||||||
Facebook
|
{platformInfo.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -477,7 +956,8 @@ const CommentList: React.FC<CommentListProps> = () => {
|
|||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -489,6 +969,26 @@ const CommentList: React.FC<CommentListProps> = () => {
|
|||||||
<CommentPreview comment={selectedComment} onClose={() => setSelectedComment(null)} />
|
<CommentPreview comment={selectedComment} onClose={() => setSelectedComment(null)} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 在评论列表的底部添加无限滚动加载指示器 */}
|
||||||
|
<div
|
||||||
|
ref={loadMoreRef}
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '20px 0',
|
||||||
|
visibility: loading || !hasMore ? 'hidden' : 'visible'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoadingMore && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Spin tip="Loading more comments..." />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!hasMore && comments.length === 0 && !loading && (
|
||||||
|
<Empty description="No comments found" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ const PostList: React.FC<PostListProps> = ({ influencerId, projectId }) => {
|
|||||||
|
|
||||||
// Handle post click to view comments
|
// Handle post click to view comments
|
||||||
const handleViewComments = (postId: string) => {
|
const handleViewComments = (postId: string) => {
|
||||||
|
console.log('Navigating to comments for post:', postId);
|
||||||
navigate(`/comments?post_id=${postId}`);
|
navigate(`/comments?post_id=${postId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export interface Comment {
|
export interface Comment {
|
||||||
id: string;
|
id: string;
|
||||||
platform: 'facebook' | 'threads' | 'instagram' | 'linkedin' | 'xiaohongshu' | 'youtube';
|
platform: 'facebook' | 'threads' | 'instagram' | 'linkedin' | 'xiaohongshu' | 'youtube' | 'twitter';
|
||||||
contentType?: 'post' | 'reel' | 'video' | 'short';
|
contentType?: 'post' | 'reel' | 'video' | 'short';
|
||||||
content: string;
|
content: string;
|
||||||
author: string;
|
author: string;
|
||||||
@@ -59,7 +59,7 @@ export interface ReplyPersona {
|
|||||||
export interface ReplyAccount {
|
export interface ReplyAccount {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
platform: 'facebook' | 'threads' | 'instagram' | 'linkedin' | 'xiaohongshu' | 'youtube';
|
platform: 'facebook' | 'threads' | 'instagram' | 'linkedin' | 'xiaohongshu' | 'youtube' | 'twitter';
|
||||||
avatar: string;
|
avatar: string;
|
||||||
role: 'admin' | 'moderator' | 'support';
|
role: 'admin' | 'moderator' | 'support';
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user