mirror of
https://github.com/Funkoala14/CreatorCenter_OOIN.git
synced 2025-06-07 22:58:14 +08:00
233 lines
11 KiB
JavaScript
233 lines
11 KiB
JavaScript
import React, { useEffect, useRef, useCallback } from 'react';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { Table, Form, Spinner } from 'react-bootstrap';
|
|
import { ChevronUp, ChevronDown } from 'lucide-react';
|
|
import {
|
|
toggleCreatorSelection,
|
|
selectAllCreators,
|
|
clearCreatorSelection,
|
|
fetchPrivateCreators,
|
|
} from '../store/slices/creatorsSlice';
|
|
import { setSortBy } from '../store/slices/filtersSlice';
|
|
import '../styles/DatabaseList.scss';
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import { Link } from 'react-router-dom';
|
|
|
|
export default function PrivateCreatorList({ path }) {
|
|
const dispatch = useDispatch();
|
|
const { privateCreators, status, selectedCreators, hasMore, isLoadingMore, pagination, error } = useSelector(
|
|
(state) => state.creators
|
|
);
|
|
const { sortBy, sortDirection } = useSelector((state) => state.filters);
|
|
const observer = useRef();
|
|
const loadingRef = useCallback(
|
|
(node) => {
|
|
if (isLoadingMore) return;
|
|
if (observer.current) observer.current.disconnect();
|
|
observer.current = new IntersectionObserver((entries) => {
|
|
if (entries[0].isIntersecting && hasMore) {
|
|
const nextPage = pagination?.current_page + 1;
|
|
dispatch(fetchPrivateCreators({ path, page: nextPage }));
|
|
}
|
|
});
|
|
if (node) observer.current.observe(node);
|
|
},
|
|
[isLoadingMore, hasMore, pagination, path, dispatch]
|
|
);
|
|
|
|
// 组件加载时获取创作者数据
|
|
useEffect(() => {
|
|
dispatch(fetchPrivateCreators({ path, page: 1 }));
|
|
}, [path,dispatch]);
|
|
|
|
useEffect(() => {
|
|
console.log(privateCreators);
|
|
}, [privateCreators]);
|
|
|
|
// 处理全选/取消全选
|
|
const handleSelectAll = (e) => {
|
|
if (e.target.checked) {
|
|
dispatch(selectAllCreators('database'));
|
|
} else {
|
|
dispatch(clearCreatorSelection());
|
|
}
|
|
};
|
|
|
|
// 处理单个创作者选择
|
|
const handleSelectCreator = (creatorId) => {
|
|
dispatch(toggleCreatorSelection(creatorId));
|
|
};
|
|
|
|
// 处理排序
|
|
const handleSort = (field) => {
|
|
return;
|
|
dispatch(setSortBy(field));
|
|
};
|
|
|
|
// 渲染排序图标
|
|
const renderSortIcon = (field) => {
|
|
return;
|
|
if (sortBy === field) {
|
|
return sortDirection === 'asc' ? <ChevronUp size={14} /> : <ChevronDown size={14} />;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// 根据类别获取样式类名
|
|
const getCategoryClassName = (category) => {
|
|
const categoryMap = {
|
|
'Phones & Electronics': 'phones',
|
|
'Womenswear & Underwear': 'women',
|
|
'Sports & Outdoor': 'sports',
|
|
'Food & Beverage': 'food',
|
|
Health: 'health',
|
|
Kitchenware: 'kitchen',
|
|
};
|
|
|
|
return categoryMap[category] || '';
|
|
};
|
|
|
|
// 如果正在加载且没有数据,显示加载中
|
|
if (status === 'loading' && !isLoadingMore) {
|
|
return (
|
|
<div className='text-center p-5'>
|
|
<Spinner animation='border' role='status' variant='primary'>
|
|
<span className='visually-hidden'>Loading...</span>
|
|
</Spinner>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 如果加载失败,显示错误信息
|
|
if (status === 'failed') {
|
|
return <div className='alert alert-danger'>{error || 'Failed to load creators. Please try again later.'}</div>;
|
|
}
|
|
|
|
return (
|
|
<div className='creator-database-table'>
|
|
<div className='table-container'>
|
|
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden'>
|
|
<thead className='sticky-header'>
|
|
<tr>
|
|
<th className='selector' style={{ width: '40px' }}>
|
|
<Form.Check
|
|
type='checkbox'
|
|
checked={selectedCreators.length === privateCreators.length && privateCreators.length > 0}
|
|
onChange={handleSelectAll}
|
|
/>
|
|
</th>
|
|
<th className='creator' onClick={() => handleSort('name')} style={{ width: '180px' }}>
|
|
Creator {renderSortIcon('name')}
|
|
</th>
|
|
<th
|
|
className='category text-center'
|
|
onClick={() => handleSort('category')}
|
|
style={{ width: '180px' }}
|
|
>
|
|
Category {renderSortIcon('category')}
|
|
</th>
|
|
<th className='e-commerce-level text-center' onClick={() => handleSort('e_commerce_level')}>
|
|
E-commerce Level {renderSortIcon('e_commerce_level')}
|
|
</th>
|
|
<th className='exposure-level text-center' onClick={() => handleSort('exposure_level')}>
|
|
Exposure Level {renderSortIcon('exposure_level')}
|
|
</th>
|
|
<th className='followers text-center' onClick={() => handleSort('followers')}>
|
|
Followers {renderSortIcon('followers')}
|
|
</th>
|
|
<th className='gmv text-center' onClick={() => handleSort('gmv')}>
|
|
GMV {renderSortIcon('gmv')}
|
|
</th>
|
|
<th className='views text-center' onClick={() => handleSort('avg_video_views')}>
|
|
Avg. Video Views {renderSortIcon('avg_video_views')}
|
|
</th>
|
|
<th className='pricing text-center'>Pricing</th>
|
|
<th className='collab-count text-center'># Collab</th>
|
|
<th className='latest-collab text-center'>Latest Collab.</th>
|
|
<th className='e-commerce text-center'>E-commerce</th>
|
|
<th className='profile text-center'>Profile</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{!privateCreators || privateCreators.length <= 0 ? (
|
|
<tr>
|
|
<td colSpan='13' className='text-center py-4'>
|
|
No creators found matching your filters.
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
privateCreators.map((creator) => (
|
|
<tr
|
|
key={creator.creator_id}
|
|
className={selectedCreators.includes(creator.creator_id) ? 'selected' : ''}
|
|
>
|
|
<td>
|
|
<Form.Check
|
|
type='checkbox'
|
|
checked={selectedCreators.includes(creator.creator_id)}
|
|
onChange={() => handleSelectCreator(creator.creator_id)}
|
|
/>
|
|
</td>
|
|
<td className='creator-cell'>
|
|
<div className='d-flex align-items-center'>
|
|
<div className='creator-avatar'>
|
|
<img src={creator.avatar} alt={creator.name} />
|
|
{creator.status && <span className='verified-badge'></span>}
|
|
</div>
|
|
<Link to={`/creator/${creator.creator_id}`} className='creator-name'>
|
|
{creator.name}
|
|
</Link>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span className={`category-pill ${getCategoryClassName(creator.category)}`}>
|
|
{creator.category}
|
|
</span>
|
|
</td>
|
|
<td className='text-center'>
|
|
<span className='level-badge ecommerce-level'>{creator.e_commerce_level}</span>
|
|
</td>
|
|
<td className='text-center'>
|
|
<span
|
|
className='level-badge exposure-level'
|
|
data-level={creator.exposure_level}
|
|
>
|
|
{creator.exposure_level}
|
|
</span>
|
|
</td>
|
|
<td className='text-nowrap text-center'>{creator.followers}</td>
|
|
<td className='text-center'>
|
|
<div>{creator.gmv}</div>
|
|
<div className='small text-muted'>Items Sold: {creator.soldPercentage}</div>
|
|
</td>
|
|
<td className='text-nowrap text-center'>{creator.avg_video_views}</td>
|
|
<td className='text-center'>{creator.pricing}</td>
|
|
<td className='text-center'>{creator.collab_count}</td>
|
|
<td className='text-center'>{creator.latestCollab}</td>
|
|
<td className='text-center'>
|
|
{creator.hasEcommerce ? <div className='colored-dot blue mx-auto'></div> : null}
|
|
</td>
|
|
<td className='text-center'>
|
|
{creator.hasTiktok && (
|
|
<div className='social-icon tiktok-icon mx-auto'>
|
|
<FontAwesomeIcon icon='fa-brands fa-tiktok' />
|
|
</div>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</Table>
|
|
{hasMore && (
|
|
<div ref={loadingRef} className='text-center p-3'>
|
|
<Spinner animation='border' role='status' variant='primary' size='sm'>
|
|
<span className='visually-hidden'>Loading more...</span>
|
|
</Spinner>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|