mirror of
https://github.com/Funkoala14/CreatorCenter_OOIN.git
synced 2025-06-08 02:58:14 +08:00
[dev]database list
This commit is contained in:
parent
d9e0750441
commit
1f97455961
62
package-lock.json
generated
62
package-lock.json
generated
@ -14,6 +14,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@reduxjs/toolkit": "^2.8.1",
|
||||
"bootstrap": "^5.3.3",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.508.0",
|
||||
@ -1396,6 +1397,31 @@
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.1.tgz",
|
||||
"integrity": "sha512-GLjHS13LiBdiuxSJvfWs3+Cx5yt97mCbuVlDteTusS6VRksPhoWviO8L1e3Re1G94m6lkw/l4pjEEyyNaGf19g==",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@standard-schema/utils": "^0.3.0",
|
||||
"immer": "^10.0.3",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"reselect": "^5.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@restart/hooks": {
|
||||
"version": "0.4.16",
|
||||
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
|
||||
@ -1706,6 +1732,16 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
@ -2955,6 +2991,15 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
|
||||
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
|
||||
@ -4041,9 +4086,20 @@
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||
"peerDependencies": {
|
||||
"redux": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
|
@ -16,6 +16,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@reduxjs/toolkit": "^2.8.1",
|
||||
"bootstrap": "^5.3.3",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.508.0",
|
||||
|
@ -1,10 +1,23 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Card, Row, Col, Form, InputGroup, Button } from 'react-bootstrap';
|
||||
import { Eye } from 'lucide-react';
|
||||
import RangeSlider from './RangeSlider';
|
||||
import {
|
||||
setCategory,
|
||||
toggleEcommerceRating,
|
||||
toggleExposureRating,
|
||||
toggleGmvRange,
|
||||
setViewsRange,
|
||||
resetFilters,
|
||||
} from '../store/slices/filtersSlice';
|
||||
import { fetchCreators } from '../store/slices/creatorsSlice';
|
||||
import '../styles/DatabaseFilter.scss';
|
||||
|
||||
export default function DatabaseFilter() {
|
||||
export default function DatabaseFilter({ path }) {
|
||||
const dispatch = useDispatch();
|
||||
const filters = useSelector((state) => state.filters);
|
||||
|
||||
// 类别选项数据
|
||||
const categories = [
|
||||
'Phones & Electronics',
|
||||
@ -50,54 +63,44 @@ export default function DatabaseFilter() {
|
||||
return closestIndex;
|
||||
};
|
||||
|
||||
// 存储选择的状态
|
||||
const [selectedCategory, setSelectedCategory] = useState('Homes Supplies');
|
||||
const [selectedEcommerceRatings, setSelectedEcommerceRatings] = useState(['L2', 'L3']);
|
||||
const [selectedExposureRatings, setSelectedExposureRatings] = useState([]);
|
||||
const [selectedGmvRanges, setSelectedGmvRanges] = useState(['$5k - $25k', '$25k - $60k']);
|
||||
// 本地状态用于表单控制
|
||||
const [minViews, setMinViews] = useState(filters.viewsRange[0]);
|
||||
const [maxViews, setMaxViews] = useState(filters.viewsRange[1]);
|
||||
|
||||
// 视频观看量过滤器数值范围 - 使用离散值初始化
|
||||
const [viewsRange, setViewsRange] = useState([0, 100000]);
|
||||
const [minViews, setMinViews] = useState(0);
|
||||
const [maxViews, setMaxViews] = useState(100000);
|
||||
// 监听Redux状态变化,更新本地表单状态
|
||||
useEffect(() => {
|
||||
setMinViews(filters.viewsRange[0]);
|
||||
setMaxViews(filters.viewsRange[1]);
|
||||
}, [filters.viewsRange]);
|
||||
|
||||
// 组件加载时获取数据
|
||||
useEffect(() => {
|
||||
dispatch(fetchCreators({ path }));
|
||||
}, [dispatch, filters]);
|
||||
|
||||
// 处理类别选择
|
||||
const handleCategorySelect = (category) => {
|
||||
setSelectedCategory(category);
|
||||
dispatch(setCategory(category));
|
||||
};
|
||||
|
||||
// 处理电商评级选择
|
||||
const handleEcommerceRatingSelect = (rating) => {
|
||||
if (selectedEcommerceRatings.includes(rating)) {
|
||||
setSelectedEcommerceRatings(selectedEcommerceRatings.filter((r) => r !== rating));
|
||||
} else {
|
||||
setSelectedEcommerceRatings([...selectedEcommerceRatings, rating]);
|
||||
}
|
||||
dispatch(toggleEcommerceRating(rating));
|
||||
};
|
||||
|
||||
// 处理曝光评级选择
|
||||
const handleExposureRatingSelect = (rating) => {
|
||||
if (selectedExposureRatings.includes(rating)) {
|
||||
setSelectedExposureRatings(selectedExposureRatings.filter((r) => r !== rating));
|
||||
} else {
|
||||
setSelectedExposureRatings([...selectedExposureRatings, rating]);
|
||||
}
|
||||
dispatch(toggleExposureRating(rating));
|
||||
};
|
||||
|
||||
// 处理GMV范围选择
|
||||
const handleGmvRangeSelect = (range) => {
|
||||
if (selectedGmvRanges.includes(range)) {
|
||||
setSelectedGmvRanges(selectedGmvRanges.filter((r) => r !== range));
|
||||
} else {
|
||||
setSelectedGmvRanges([...selectedGmvRanges, range]);
|
||||
}
|
||||
dispatch(toggleGmvRange(range));
|
||||
};
|
||||
|
||||
// 处理视图范围更新
|
||||
const handleViewsRangeChange = (newRange) => {
|
||||
setViewsRange(newRange);
|
||||
setMinViews(newRange[0]);
|
||||
setMaxViews(newRange[1]);
|
||||
dispatch(setViewsRange(newRange));
|
||||
};
|
||||
|
||||
// 处理min input变更
|
||||
@ -123,7 +126,7 @@ export default function DatabaseFilter() {
|
||||
const finalValue = Math.min(discreteValue, maxViews);
|
||||
|
||||
setMinViews(finalValue);
|
||||
setViewsRange([finalValue, viewsRange[1]]);
|
||||
dispatch(setViewsRange([finalValue, filters.viewsRange[1]]));
|
||||
} else {
|
||||
// 找到最接近的离散值
|
||||
const closestIndex = findClosestDiscreteIndex(maxViews);
|
||||
@ -133,7 +136,7 @@ export default function DatabaseFilter() {
|
||||
const finalValue = Math.max(discreteValue, minViews);
|
||||
|
||||
setMaxViews(finalValue);
|
||||
setViewsRange([viewsRange[0], finalValue]);
|
||||
dispatch(setViewsRange([filters.viewsRange[0], finalValue]));
|
||||
}
|
||||
};
|
||||
|
||||
@ -149,7 +152,7 @@ export default function DatabaseFilter() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className='shadow-sm mb-4 filter-card'>
|
||||
<div className='shadow-sm mb-4 filter-card'>
|
||||
<Card.Body>
|
||||
<h3 className='mb-4'>Filter</h3>
|
||||
|
||||
@ -160,7 +163,7 @@ export default function DatabaseFilter() {
|
||||
{categories.map((category) => (
|
||||
<Button
|
||||
key={category}
|
||||
variant={selectedCategory === category ? 'primary' : 'light'}
|
||||
variant={filters.category.includes(category) ? 'primary' : 'light'}
|
||||
className='rounded-pill'
|
||||
onClick={() => handleCategorySelect(category)}
|
||||
>
|
||||
@ -177,7 +180,7 @@ export default function DatabaseFilter() {
|
||||
{ecommerceRatings.map((rating) => (
|
||||
<Button
|
||||
key={rating}
|
||||
variant={selectedEcommerceRatings.includes(rating) ? 'primary' : 'light'}
|
||||
variant={filters.ecommerceRatings.includes(rating) ? 'primary' : 'light'}
|
||||
className='rounded-pill'
|
||||
onClick={() => handleEcommerceRatingSelect(rating)}
|
||||
>
|
||||
@ -194,7 +197,7 @@ export default function DatabaseFilter() {
|
||||
{exposureRatings.map((rating) => (
|
||||
<Button
|
||||
key={rating}
|
||||
variant={selectedExposureRatings.includes(rating) ? 'primary' : 'light'}
|
||||
variant={filters.exposureRatings.includes(rating) ? 'primary' : 'light'}
|
||||
className='rounded-pill'
|
||||
onClick={() => handleExposureRatingSelect(rating)}
|
||||
>
|
||||
@ -211,7 +214,7 @@ export default function DatabaseFilter() {
|
||||
{gmvRanges.map((range) => (
|
||||
<Button
|
||||
key={range.label}
|
||||
variant={selectedGmvRanges.includes(range.label) ? 'primary' : 'light'}
|
||||
variant={filters.gmvRanges.includes(range.label) ? 'primary' : 'light'}
|
||||
className='rounded-pill'
|
||||
onClick={() => handleGmvRangeSelect(range.label)}
|
||||
>
|
||||
@ -225,7 +228,12 @@ export default function DatabaseFilter() {
|
||||
<div className='filter-item'>
|
||||
<h5 className='filter-title'>Views</h5>
|
||||
<div className='filter-options filter-views'>
|
||||
<RangeSlider min={0} max={500000} value={viewsRange} onChange={handleViewsRangeChange} />
|
||||
<RangeSlider
|
||||
min={0}
|
||||
max={500000}
|
||||
value={filters.viewsRange}
|
||||
onChange={handleViewsRangeChange}
|
||||
/>
|
||||
|
||||
<div className='range-input'>
|
||||
<InputGroup>
|
||||
@ -255,6 +263,6 @@ export default function DatabaseFilter() {
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,189 @@
|
||||
export default function DatabaseList() {
|
||||
return <div>DatabaseList</div>;
|
||||
}
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Table, Form, Spinner } from 'react-bootstrap';
|
||||
import { ChevronUp, ChevronDown } from 'lucide-react';
|
||||
import {
|
||||
fetchCreators,
|
||||
toggleCreatorSelection,
|
||||
selectAllCreators,
|
||||
clearCreatorSelection,
|
||||
} from '../store/slices/creatorsSlice';
|
||||
import { setSortBy } from '../store/slices/filtersSlice';
|
||||
import '../styles/DatabaseList.scss';
|
||||
|
||||
export default function DatabaseList({ path }) {
|
||||
const dispatch = useDispatch();
|
||||
const { creators, status, selectedCreators } = useSelector((state) => state.creators);
|
||||
const { sortBy, sortDirection } = useSelector((state) => state.filters);
|
||||
|
||||
// 组件加载时获取创作者数据
|
||||
useEffect(() => {
|
||||
if (status === 'idle') {
|
||||
dispatch(fetchCreators({path}));
|
||||
}
|
||||
}, [dispatch, status]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(creators);
|
||||
}, [creators]);
|
||||
// 处理全选/取消全选
|
||||
const handleSelectAll = (e) => {
|
||||
if (e.target.checked) {
|
||||
dispatch(selectAllCreators());
|
||||
} else {
|
||||
dispatch(clearCreatorSelection());
|
||||
}
|
||||
};
|
||||
|
||||
// 处理单个创作者选择
|
||||
const handleSelectCreator = (creatorId) => {
|
||||
dispatch(toggleCreatorSelection(creatorId));
|
||||
};
|
||||
|
||||
// 处理排序
|
||||
const handleSort = (field) => {
|
||||
dispatch(setSortBy(field));
|
||||
};
|
||||
|
||||
// 渲染排序图标
|
||||
const renderSortIcon = (field) => {
|
||||
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') {
|
||||
return (
|
||||
<div className='text-center p-5'>
|
||||
<Spinner animation='border' role='status'>
|
||||
<span className='visually-hidden'>Loading...</span>
|
||||
</Spinner>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果加载失败,显示错误信息
|
||||
if (status === 'failed') {
|
||||
return <div className='alert alert-danger'>Failed to load creators. Please try again later.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='creator-database-table'>
|
||||
<Table responsive hover className='bg-white shadow-sm rounded overflow-hidden'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className='selector' style={{ width: '40px' }}>
|
||||
<Form.Check
|
||||
type='checkbox'
|
||||
checked={selectedCreators.length === creators.length && creators.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('ecommerceLevel')}>
|
||||
E-commerce Level {renderSortIcon('ecommerceLevel')}
|
||||
</th>
|
||||
<th className='exposure-level text-center' onClick={() => handleSort('exposureLevel')}>
|
||||
Exposure Level {renderSortIcon('exposureLevel')}
|
||||
</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('avgViews')}>
|
||||
Avg. Video Views {renderSortIcon('avgViews')}
|
||||
</th>
|
||||
<th className='e-commerce text-center'>E-commerce</th>
|
||||
<th className='profile text-center'>Profile</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{creators.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan='10' className='text-center py-4'>
|
||||
No creators found matching your filters.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
creators.map((creator) => (
|
||||
<tr key={creator.id} className={selectedCreators.includes(creator.id) ? 'selected' : ''}>
|
||||
<td>
|
||||
<Form.Check
|
||||
type='checkbox'
|
||||
checked={selectedCreators.includes(creator.id)}
|
||||
onChange={() => handleSelectCreator(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.verified && <span className='verified-badge'></span>}
|
||||
</div>
|
||||
<div className='creator-name'>{creator.name}</div>
|
||||
</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.ecommerceLevel}</span>
|
||||
</td>
|
||||
<td className='text-center'>
|
||||
<span className='level-badge exposure-level' data-level={creator.exposureLevel}>
|
||||
{creator.exposureLevel}
|
||||
</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.avgViews}</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'>
|
||||
<i className='fab fa-tiktok'></i>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Nav, Accordion } from 'react-bootstrap';
|
||||
import { Settings, ChevronDown, Blocks, SquareActivity, LayoutDashboard, Mail, UserSearch, Heart } from 'lucide-react';
|
||||
import { Settings, ChevronDown, Blocks, SquareActivity, LayoutDashboard, Mail, UserSearch, Heart, Send, FileText } from 'lucide-react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import logo from '@/assets/logo.png';
|
||||
import '../styles/sidebar.scss';
|
||||
@ -12,33 +12,33 @@ const menuItems = [
|
||||
id: 'creator-discovery',
|
||||
title: 'Creator Discovery',
|
||||
path: '/creator-discovery',
|
||||
icon: <UserSearch style={{ width: 20, height: 20 }} />,
|
||||
icon: <UserSearch />,
|
||||
hasSubmenu: false,
|
||||
},
|
||||
{
|
||||
id: 'creator-database',
|
||||
title: 'Creator Database',
|
||||
path: '/creator-database',
|
||||
icon: <Blocks style={{ width: 20, height: 20 }} />,
|
||||
icon: <Blocks/>,
|
||||
hasSubmenu: true,
|
||||
submenuItems: [
|
||||
{
|
||||
id: 'tiktok',
|
||||
title: 'TikTok',
|
||||
path: '/creator-database/tiktok',
|
||||
icon: <FontAwesomeIcon style={{ width: 20, height: 20 }} icon='fa-brands fa-tiktok' />,
|
||||
icon: <FontAwesomeIcon icon='fa-brands fa-tiktok' />,
|
||||
},
|
||||
{
|
||||
id: 'instagram',
|
||||
title: 'Instagram',
|
||||
path: '/creator-database/instagram',
|
||||
icon: <FontAwesomeIcon style={{ width: 20, height: 20 }} icon='fa-brands fa-instagram' />,
|
||||
icon: <FontAwesomeIcon icon='fa-brands fa-instagram' />,
|
||||
},
|
||||
{
|
||||
id: 'youtube',
|
||||
title: 'YouTube',
|
||||
path: '/creator-database/youtube',
|
||||
icon: <FontAwesomeIcon style={{ width: 20, height: 20 }} icon='fa-brands fa-youtube' />,
|
||||
icon: <FontAwesomeIcon icon='fa-brands fa-youtube' />,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -46,7 +46,7 @@ const menuItems = [
|
||||
id: 'private-creators',
|
||||
title: 'Private Creators',
|
||||
path: '/private-creators',
|
||||
icon: <Heart style={{ width: 20, height: 20 }} />,
|
||||
icon: <Heart />,
|
||||
hasSubmenu: true,
|
||||
submenuItems: [],
|
||||
},
|
||||
@ -54,29 +54,42 @@ const menuItems = [
|
||||
id: 'deep-analysis',
|
||||
title: 'Deep Analysis',
|
||||
path: '/deep-analysis',
|
||||
icon: <SquareActivity style={{ width: 20, height: 20 }} />,
|
||||
icon: <SquareActivity />,
|
||||
hasSubmenu: false,
|
||||
},
|
||||
{
|
||||
id: 'brands',
|
||||
title: 'Brands',
|
||||
path: '/brands',
|
||||
icon: <LayoutDashboard style={{ width: 20, height: 20 }} />,
|
||||
icon: <LayoutDashboard />,
|
||||
hasSubmenu: false,
|
||||
},
|
||||
{
|
||||
id: 'creator-inbox',
|
||||
title: 'Creator Inbox',
|
||||
path: '/creator-inbox',
|
||||
icon: <Mail style={{ width: 20, height: 20 }} />,
|
||||
icon: <Mail />,
|
||||
hasSubmenu: true,
|
||||
submenuItems: [],
|
||||
submenuItems: [
|
||||
{
|
||||
id: 'inbox',
|
||||
title: 'Inbox',
|
||||
path: '/creator-inbox',
|
||||
icon: <Send/>,
|
||||
},
|
||||
{
|
||||
id: 'templates',
|
||||
title: 'Templates',
|
||||
path: '/creator-inbox/templates',
|
||||
icon: <FileText />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
title: 'Settings',
|
||||
path: '/settings',
|
||||
icon: <Settings style={{ width: 20, height: 20 }} />,
|
||||
icon: <Settings />,
|
||||
hasSubmenu: false,
|
||||
},
|
||||
];
|
||||
|
@ -1,11 +1,15 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Provider } from 'react-redux';
|
||||
import store from './store';
|
||||
import './styles/global.scss';
|
||||
import './index.css';
|
||||
import App from './App.jsx';
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</StrictMode>
|
||||
);
|
||||
|
4
src/pages/Brands.jsx
Normal file
4
src/pages/Brands.jsx
Normal file
@ -0,0 +1,4 @@
|
||||
export default function Brands() {
|
||||
return <div>Brands</div>;
|
||||
}
|
||||
|
4
src/pages/CreatorInbox.jsx
Normal file
4
src/pages/CreatorInbox.jsx
Normal file
@ -0,0 +1,4 @@
|
||||
export default function CreatorInbox() {
|
||||
return <div>CreatorInbox</div>;
|
||||
}
|
||||
|
@ -9,8 +9,8 @@ export default function Database({ path }) {
|
||||
<div className='breadcrumb-item'>Creator Database</div>
|
||||
<div className='breadcrumb-item'>{path}</div>
|
||||
</div>
|
||||
<DatabaseFilter />
|
||||
<DatabaseList />
|
||||
<DatabaseFilter path={path} />
|
||||
<DatabaseList path={path} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
import Home from '../pages/Home';
|
||||
import Database from '../pages/Database';
|
||||
import MainLayout from '../components/MainLayout';
|
||||
import Brands from '../pages/Brands';
|
||||
import CreatorInbox from '../pages/CreatorInbox';
|
||||
|
||||
// Routes configuration object
|
||||
const routes = [
|
||||
@ -44,11 +46,20 @@ const routes = [
|
||||
},
|
||||
{
|
||||
path: '/brands',
|
||||
element: <Home />,
|
||||
element: <Brands />,
|
||||
},
|
||||
{
|
||||
path: '/creator-inbox/*',
|
||||
element: <Home />,
|
||||
path: '/creator-inbox',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
element: <CreatorInbox />,
|
||||
},
|
||||
{
|
||||
path: 'templates',
|
||||
element: <Home />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
|
16
src/store/index.js
Normal file
16
src/store/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import creatorsReducer from './slices/creatorsSlice';
|
||||
import filtersReducer from './slices/filtersSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
creators: creatorsReducer,
|
||||
filters: filtersReducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
serializableCheck: false,
|
||||
}),
|
||||
});
|
||||
|
||||
export default store;
|
204
src/store/slices/creatorsSlice.js
Normal file
204
src/store/slices/creatorsSlice.js
Normal file
@ -0,0 +1,204 @@
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
|
||||
// 模拟创作者数据,实际项目中会从API获取
|
||||
const mockCreators = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'name',
|
||||
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=1',
|
||||
category: 'Phones & Electronics',
|
||||
ecommerceLevel: 'L2',
|
||||
exposureLevel: 'KOC-1',
|
||||
followers: '162.2k',
|
||||
gmv: '$534.1k',
|
||||
soldPercentage: '18.1%',
|
||||
avgViews: '1.9k',
|
||||
hasEcommerce: true,
|
||||
hasTiktok: true,
|
||||
verified: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'name',
|
||||
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=2',
|
||||
category: 'Womenswear & Underwear',
|
||||
ecommerceLevel: 'L3',
|
||||
exposureLevel: 'KOL-3',
|
||||
followers: '162.2k',
|
||||
gmv: '$534.1k',
|
||||
soldPercentage: '18.1%',
|
||||
avgViews: '1.9k',
|
||||
hasEcommerce: false,
|
||||
hasTiktok: true,
|
||||
verified: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'name',
|
||||
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=3',
|
||||
category: 'Sports & Outdoor',
|
||||
ecommerceLevel: 'L4',
|
||||
exposureLevel: 'KOC-2',
|
||||
followers: '162.2k',
|
||||
gmv: '$534.1k',
|
||||
soldPercentage: '18.1%',
|
||||
avgViews: '1.9k',
|
||||
hasEcommerce: true,
|
||||
hasTiktok: true,
|
||||
verified: false,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'name',
|
||||
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=4',
|
||||
category: 'Food & Beverage',
|
||||
ecommerceLevel: 'L1',
|
||||
exposureLevel: 'KOC-2',
|
||||
followers: '162.2k',
|
||||
gmv: '$534.1k',
|
||||
soldPercentage: '18.1%',
|
||||
avgViews: '1.9k',
|
||||
hasEcommerce: true,
|
||||
hasTiktok: true,
|
||||
hasInstagram: true,
|
||||
hasYoutube: true,
|
||||
verified: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'name',
|
||||
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=5',
|
||||
category: 'Health',
|
||||
ecommerceLevel: 'L5',
|
||||
exposureLevel: 'KOL-2',
|
||||
followers: '162.2k',
|
||||
gmv: '$534.1k',
|
||||
soldPercentage: '18.1%',
|
||||
avgViews: '1.9k',
|
||||
hasEcommerce: false,
|
||||
hasTiktok: true,
|
||||
hasInstagram: true,
|
||||
hasYoutube: true,
|
||||
verified: true,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'name',
|
||||
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=6',
|
||||
category: 'Kitchenware',
|
||||
ecommerceLevel: 'New tag',
|
||||
exposureLevel: 'New tag',
|
||||
followers: '162.2k',
|
||||
gmv: '$534.1k',
|
||||
soldPercentage: '18.1%',
|
||||
avgViews: '1.9k',
|
||||
hasEcommerce: true,
|
||||
hasTiktok: true,
|
||||
hasInstagram: true,
|
||||
hasYoutube: true,
|
||||
verified: false,
|
||||
},
|
||||
];
|
||||
|
||||
// 模拟API获取数据的异步Thunk
|
||||
export const fetchCreators = createAsyncThunk('creators/fetchCreators', async ({ path }, { getState }) => {
|
||||
// 模拟API调用延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// 获取当前的筛选条件
|
||||
const state = getState();
|
||||
const filters = state.filters;
|
||||
|
||||
// 应用筛选逻辑(实际项目中可能在服务器端进行)
|
||||
let filteredCreators = [...mockCreators];
|
||||
console.log(filters);
|
||||
|
||||
// 如果有选定的类别,进行筛选
|
||||
if (filters.category.length > 0) {
|
||||
filteredCreators = filteredCreators.filter((creator) => filters.category.includes(creator.category));
|
||||
}
|
||||
|
||||
// 如果有选定的电商评级,进行筛选
|
||||
if (filters.ecommerceRatings.length > 0) {
|
||||
filteredCreators = filteredCreators.filter((creator) =>
|
||||
filters.ecommerceRatings.includes(creator.ecommerceLevel)
|
||||
);
|
||||
}
|
||||
|
||||
// 如果有选定的曝光评级,进行筛选
|
||||
if (filters.exposureRatings.length > 0) {
|
||||
filteredCreators = filteredCreators.filter((creator) =>
|
||||
filters.exposureRatings.includes(creator.exposureLevel)
|
||||
);
|
||||
}
|
||||
|
||||
// 筛选观看量范围
|
||||
if (filters.viewsRange.length === 2) {
|
||||
const minViews = filters.viewsRange[0];
|
||||
const maxViews = filters.viewsRange[1];
|
||||
|
||||
filteredCreators = filteredCreators.filter((creator) => {
|
||||
// 将带k的字符串转换为数字
|
||||
const viewsStr = creator.avgViews;
|
||||
let views = parseFloat(viewsStr);
|
||||
if (viewsStr.includes('k')) {
|
||||
views *= 1000;
|
||||
} else if (viewsStr.includes('M')) {
|
||||
views *= 1000000;
|
||||
}
|
||||
|
||||
return views >= minViews && views <= maxViews;
|
||||
});
|
||||
}
|
||||
|
||||
return filteredCreators;
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
creators: [],
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
selectedCreators: [],
|
||||
};
|
||||
|
||||
const creatorsSlice = createSlice({
|
||||
name: 'creators',
|
||||
initialState,
|
||||
reducers: {
|
||||
toggleCreatorSelection: (state, action) => {
|
||||
const creatorId = action.payload;
|
||||
const isSelected = state.selectedCreators.includes(creatorId);
|
||||
|
||||
if (isSelected) {
|
||||
state.selectedCreators = state.selectedCreators.filter((id) => id !== creatorId);
|
||||
} else {
|
||||
state.selectedCreators.push(creatorId);
|
||||
}
|
||||
},
|
||||
selectAllCreators: (state) => {
|
||||
state.selectedCreators = state.creators.map((creator) => creator.id);
|
||||
},
|
||||
clearCreatorSelection: (state) => {
|
||||
state.selectedCreators = [];
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchCreators.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
})
|
||||
.addCase(fetchCreators.fulfilled, (state, action) => {
|
||||
state.status = 'succeeded';
|
||||
state.creators = action.payload;
|
||||
})
|
||||
.addCase(fetchCreators.rejected, (state, action) => {
|
||||
console.log(action);
|
||||
state.status = 'failed';
|
||||
state.error = action.error.message;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { toggleCreatorSelection, selectAllCreators, clearCreatorSelection } = creatorsSlice.actions;
|
||||
|
||||
export default creatorsSlice.reducer;
|
78
src/store/slices/filtersSlice.js
Normal file
78
src/store/slices/filtersSlice.js
Normal file
@ -0,0 +1,78 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const initialState = {
|
||||
category: ['Homes Supplies'],
|
||||
ecommerceRatings: ['L2', 'L3'],
|
||||
exposureRatings: [],
|
||||
gmvRanges: ['$5k - $25k', '$25k - $60k'],
|
||||
viewsRange: [0, 100000],
|
||||
sortBy: 'followers',
|
||||
sortDirection: 'desc',
|
||||
};
|
||||
|
||||
const filtersSlice = createSlice({
|
||||
name: 'filters',
|
||||
initialState,
|
||||
reducers: {
|
||||
setCategory: (state, action) => {
|
||||
const category = action.payload;
|
||||
if (state.category.includes(category)) {
|
||||
state.category = state.category.filter((r) => r !== category);
|
||||
} else {
|
||||
state.category.push(category);
|
||||
}
|
||||
},
|
||||
toggleEcommerceRating: (state, action) => {
|
||||
const rating = action.payload;
|
||||
if (state.ecommerceRatings.includes(rating)) {
|
||||
state.ecommerceRatings = state.ecommerceRatings.filter((r) => r !== rating);
|
||||
} else {
|
||||
state.ecommerceRatings.push(rating);
|
||||
}
|
||||
},
|
||||
toggleExposureRating: (state, action) => {
|
||||
const rating = action.payload;
|
||||
if (state.exposureRatings.includes(rating)) {
|
||||
state.exposureRatings = state.exposureRatings.filter((r) => r !== rating);
|
||||
} else {
|
||||
state.exposureRatings.push(rating);
|
||||
}
|
||||
},
|
||||
toggleGmvRange: (state, action) => {
|
||||
const range = action.payload;
|
||||
if (state.gmvRanges.includes(range)) {
|
||||
state.gmvRanges = state.gmvRanges.filter((r) => r !== range);
|
||||
} else {
|
||||
state.gmvRanges.push(range);
|
||||
}
|
||||
},
|
||||
setViewsRange: (state, action) => {
|
||||
state.viewsRange = action.payload;
|
||||
},
|
||||
setSortBy: (state, action) => {
|
||||
// 如果选择了当前已激活的排序项,则切换排序方向
|
||||
if (state.sortBy === action.payload) {
|
||||
state.sortDirection = state.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
// 否则设置新的排序项,并重置排序方向为降序
|
||||
state.sortBy = action.payload;
|
||||
state.sortDirection = 'desc';
|
||||
}
|
||||
},
|
||||
resetFilters: () => {
|
||||
return initialState;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setCategory,
|
||||
toggleEcommerceRating,
|
||||
toggleExposureRating,
|
||||
toggleGmvRange,
|
||||
setViewsRange,
|
||||
setSortBy,
|
||||
resetFilters,
|
||||
} = filtersSlice.actions;
|
||||
|
||||
export default filtersSlice.reducer;
|
@ -17,7 +17,7 @@
|
||||
.filter-title {
|
||||
width: 170px;
|
||||
margin: 0;
|
||||
color: $gray-500;
|
||||
color: $gray-700;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
|
186
src/styles/DatabaseList.scss
Normal file
186
src/styles/DatabaseList.scss
Normal file
@ -0,0 +1,186 @@
|
||||
@import './custom-theme.scss';
|
||||
|
||||
.creator-database-table {
|
||||
.table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
border-top: none;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1rem 0.75rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: #495057;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f1f3f5;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.75rem;
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(99, 102, 241, 0.05);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.creator-cell {
|
||||
.creator-avatar {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-right: 12px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.verified-badge {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: #10b981;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
font-size: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.creator-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.category-pill {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
font-size: 0.75rem;
|
||||
color: #495057;
|
||||
white-space: nowrap;
|
||||
|
||||
&.phones {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
&.women {
|
||||
background-color: rgba(244, 114, 182, 0.1);
|
||||
color: #f472b6;
|
||||
}
|
||||
|
||||
&.sports {
|
||||
background-color: rgba(52, 211, 153, 0.1);
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
&.food {
|
||||
background-color: rgba(251, 146, 60, 0.1);
|
||||
color: #fb923c;
|
||||
}
|
||||
|
||||
&.health {
|
||||
background-color: rgba(56, 189, 248, 0.1);
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
&.kitchen {
|
||||
background-color: rgba(168, 85, 247, 0.1);
|
||||
color: #a855f7;
|
||||
}
|
||||
}
|
||||
|
||||
.level-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&.ecommerce-level {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
&.exposure-level {
|
||||
background-color: rgba(14, 165, 233, 0.1);
|
||||
color: #0ea5e9;
|
||||
|
||||
&[data-level^="KOC"] {
|
||||
background-color: rgba(52, 211, 153, 0.1);
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
&[data-level^="KOL"] {
|
||||
background-color: rgba(251, 113, 133, 0.1);
|
||||
color: #fb7185;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.colored-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
|
||||
&.blue {
|
||||
background-color: #6366f1;
|
||||
}
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.tiktok-icon {
|
||||
font-size: 14px;
|
||||
color: #000000;
|
||||
}
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
|
||||
border-left: 4px solid transparent;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: var(--bs-primary);
|
||||
@ -64,7 +64,8 @@
|
||||
font-size: 1rem;
|
||||
box-shadow: none;
|
||||
font-weight: 500;
|
||||
|
||||
border-left: 4px solid transparent;
|
||||
|
||||
&:not(.collapsed) {
|
||||
color: var(--bs-primary);
|
||||
background-color: rgba(var(--bs-primary-rgb), 0.05);
|
||||
|
Loading…
Reference in New Issue
Block a user