[dev]creator detail->top info

This commit is contained in:
susie-laptop 2025-05-15 21:22:08 -04:00
parent 9999e334fb
commit cf1a9d10f6
20 changed files with 760 additions and 47 deletions

View File

@ -141,7 +141,7 @@ OOIN Creator Center is a React application built with Vite that allows users to
### UI Components ### UI Components
- `SearchBar`: Reusable search component used across different pages - `SearchBar`: Reusable search component used across different pages
- `DatabaseFilter`: Complex filter component for creator discovery - `DatabaseFilter`: Complex filter component for creator discovery
- `DatabaseList`: Displays creator data in a tabular format - `CreatorList`: Displays creator data in a tabular format
- `ChatWindow` and `ChatInput`: Used for creator communications - `ChatWindow` and `ChatInput`: Used for creator communications
- `RangeSlider`: Custom slider component for range-based filtering - `RangeSlider`: Custom slider component for range-based filtering

27
package-lock.json generated
View File

@ -16,12 +16,14 @@
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@reduxjs/toolkit": "^2.8.1", "@reduxjs/toolkit": "^2.8.1",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"chart.js": "^4.4.9",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.508.0", "lucide-react": "^0.508.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-bootstrap": "^2.10.9", "react-bootstrap": "^2.10.9",
"react-bootstrap-range-slider": "^3.0.8", "react-bootstrap-range-slider": "^3.0.8",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.0",
@ -1057,6 +1059,11 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="
},
"node_modules/@modelcontextprotocol/sdk": { "node_modules/@modelcontextprotocol/sdk": {
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz",
@ -2143,6 +2150,17 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chart.js": {
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
"integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@ -3999,6 +4017,15 @@
"react-dom": ">=17.0.0" "react-dom": ">=17.0.0"
} }
}, },
"node_modules/react-chartjs-2": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
"integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
"peerDependencies": {
"chart.js": "^4.1.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.1.0", "version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",

View File

@ -18,12 +18,14 @@
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@reduxjs/toolkit": "^2.8.1", "@reduxjs/toolkit": "^2.8.1",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"chart.js": "^4.4.9",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.508.0", "lucide-react": "^0.508.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-bootstrap": "^2.10.9", "react-bootstrap": "^2.10.9",
"react-bootstrap-range-slider": "^3.0.8", "react-bootstrap-range-slider": "^3.0.8",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.0",

View File

@ -3,6 +3,9 @@ import { library } from '@fortawesome/fontawesome-svg-core';
import { fas } from '@fortawesome/free-solid-svg-icons'; import { fas } from '@fortawesome/free-solid-svg-icons';
import Router from './router'; import Router from './router';
import './styles/Campaign.scss'; import './styles/Campaign.scss';
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
ChartJS.register(ArcElement, Tooltip, Legend);
// Add Font Awesome icons to library // Add Font Awesome icons to library
library.add(faTiktok, fas, faYoutube, faInstagram); library.add(faTiktok, fas, faYoutube, faInstagram);

View File

@ -11,8 +11,9 @@ import {
import { setSortBy } from '../store/slices/filtersSlice'; import { setSortBy } from '../store/slices/filtersSlice';
import '../styles/DatabaseList.scss'; import '../styles/DatabaseList.scss';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Link } from 'react-router-dom';
export default function DatabaseList({ path }) { export default function CreatorList({ path, pageType = 'database' }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { creators, status, selectedCreators } = useSelector((state) => state.creators); const { creators, status, selectedCreators } = useSelector((state) => state.creators);
const { sortBy, sortDirection } = useSelector((state) => state.filters); const { sortBy, sortDirection } = useSelector((state) => state.filters);
@ -20,7 +21,7 @@ export default function DatabaseList({ path }) {
// //
useEffect(() => { useEffect(() => {
if (status === 'idle') { if (status === 'idle') {
dispatch(fetchCreators({path})); dispatch(fetchCreators({ path }));
} }
}, [dispatch, status]); }, [dispatch, status]);
@ -121,6 +122,13 @@ export default function DatabaseList({ path }) {
<th className='views text-center' onClick={() => handleSort('avgViews')}> <th className='views text-center' onClick={() => handleSort('avgViews')}>
Avg. Video Views {renderSortIcon('avgViews')} Avg. Video Views {renderSortIcon('avgViews')}
</th> </th>
{pageType === 'private' && (
<>
<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='e-commerce text-center'>E-commerce</th>
<th className='profile text-center'>Profile</th> <th className='profile text-center'>Profile</th>
</tr> </tr>
@ -148,7 +156,9 @@ export default function DatabaseList({ path }) {
<img src={creator.avatar} alt={creator.name} /> <img src={creator.avatar} alt={creator.name} />
{creator.verified && <span className='verified-badge'></span>} {creator.verified && <span className='verified-badge'></span>}
</div> </div>
<div className='creator-name'>{creator.name}</div> <Link to={`/creator/${creator.id}`} className='creator-name'>
{creator.name}
</Link>
</div> </div>
</td> </td>
<td> <td>
@ -170,6 +180,13 @@ export default function DatabaseList({ path }) {
<div className='small text-muted'>Items Sold: {creator.soldPercentage}</div> <div className='small text-muted'>Items Sold: {creator.soldPercentage}</div>
</td> </td>
<td className='text-nowrap text-center'>{creator.avgViews}</td> <td className='text-nowrap text-center'>{creator.avgViews}</td>
{pageType === 'private' && (
<>
<td className='text-center'>{creator.pricing}</td>
<td className='text-center'>{creator.collabCount}</td>
<td className='text-center'>{creator.latestCollab}</td>
</>
)}
<td className='text-center'> <td className='text-center'>
{creator.hasEcommerce ? <div className='colored-dot blue mx-auto'></div> : null} {creator.hasEcommerce ? <div className='colored-dot blue mx-auto'></div> : null}
</td> </td>

View File

@ -9,15 +9,14 @@ import {
toggleExposureRating, toggleExposureRating,
toggleGmvRange, toggleGmvRange,
setViewsRange, setViewsRange,
resetFilters, setPricingRange,
} from '../store/slices/filtersSlice'; } from '../store/slices/filtersSlice';
import { fetchCreators } from '../store/slices/creatorsSlice'; import { fetchCreators } from '../store/slices/creatorsSlice';
import '../styles/DatabaseFilter.scss'; import '../styles/DatabaseFilter.scss';
export default function DatabaseFilter({ path }) { export default function DatabaseFilter({ path, pageType = 'database' }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const filters = useSelector((state) => state.filters); const filters = useSelector((state) => state.filters);
// //
const categories = [ const categories = [
'Phones & Electronics', 'Phones & Electronics',
@ -46,7 +45,7 @@ export default function DatabaseFilter({ path }) {
// //
const discreteValues = [0, 100, 1000, 10000, 100000, 250000, 500000]; const discreteValues = [0, 100, 1000, 10000, 100000, 250000, 500000];
const discretePricingValues = [0, 200, 400, 600, 800, 1000, 3000];
// //
const findClosestDiscreteIndex = (value) => { const findClosestDiscreteIndex = (value) => {
let closestIndex = 0; let closestIndex = 0;
@ -66,12 +65,16 @@ export default function DatabaseFilter({ path }) {
// //
const [minViews, setMinViews] = useState(filters.viewsRange[0]); const [minViews, setMinViews] = useState(filters.viewsRange[0]);
const [maxViews, setMaxViews] = useState(filters.viewsRange[1]); const [maxViews, setMaxViews] = useState(filters.viewsRange[1]);
const [minPricing, setMinPricing] = useState(filters.pricingRange[0]);
const [maxPricing, setMaxPricing] = useState(filters.pricingRange[1]);
// Redux // Redux
useEffect(() => { useEffect(() => {
setMinViews(filters.viewsRange[0]); setMinViews(filters.viewsRange[0]);
setMaxViews(filters.viewsRange[1]); setMaxViews(filters.viewsRange[1]);
}, [filters.viewsRange]); setMinPricing(filters.pricingRange[0]);
setMaxPricing(filters.pricingRange[1]);
}, [filters.viewsRange, filters.pricingRange]);
// //
useEffect(() => { useEffect(() => {
@ -115,6 +118,22 @@ export default function DatabaseFilter({ path }) {
setMaxViews(inputValue); setMaxViews(inputValue);
}; };
const handlePricingRangeChange = (newRange) => {
dispatch(setPricingRange(newRange));
};
// min pricing input
const handleMinPricingChange = (e) => {
const inputValue = parseInt(e.target.value) || 0;
setMinPricing(inputValue);
};
// max pricing input
const handleMaxPricingChange = (e) => {
const inputValue = parseInt(e.target.value) || 0;
setMaxPricing(inputValue);
};
// //
const handleInputBlur = (type) => { const handleInputBlur = (type) => {
if (type === 'min') { if (type === 'min') {
@ -140,6 +159,29 @@ export default function DatabaseFilter({ path }) {
} }
}; };
// min pricing input
const handlePricingInputBlur = (type) => {
if (type === 'min') {
const closestIndex = findClosestDiscreteIndex(minPricing);
const discreteValue = discretePricingValues[closestIndex];
//
const finalValue = Math.min(discreteValue, maxPricing);
setMinPricing(finalValue);
dispatch(setPricingRange([finalValue, filters.pricingRange[1]]));
} else {
const closestIndex = findClosestDiscreteIndex(maxPricing);
const discreteValue = discretePricingValues[closestIndex];
//
const finalValue = Math.max(discreteValue, minPricing);
setMaxPricing(finalValue);
dispatch(setPricingRange([filters.pricingRange[0], finalValue]));
}
};
// //
const formatValue = (value) => { const formatValue = (value) => {
if (value >= 1000000) { if (value >= 1000000) {
@ -227,7 +269,7 @@ export default function DatabaseFilter({ path }) {
{/* 视频观看量筛选 */} {/* 视频观看量筛选 */}
<div className='filter-item'> <div className='filter-item'>
<h5 className='filter-title'>Views</h5> <h5 className='filter-title'>Views</h5>
<div className='filter-options filter-views'> <div className='filter-options filter-views filter-range-slider'>
<RangeSlider <RangeSlider
min={0} min={0}
max={500000} max={500000}
@ -262,6 +304,47 @@ export default function DatabaseFilter({ path }) {
</div> </div>
</div> </div>
</div> </div>
{/* Pricing 筛选 */}
{pageType === 'private' && (
<div className='filter-item'>
<h5 className='filter-title'>Pricing</h5>
<div className='filter-options filter-pricing filter-range-slider'>
<RangeSlider
min={0}
max={500000}
value={filters.pricingRange}
onChange={handlePricingRangeChange}
/>
<div className='range-input'>
<InputGroup>
<InputGroup.Text>
<Eye size={16} />
</InputGroup.Text>
<Form.Control
type='number'
value={minPricing}
onChange={handleMinPricingChange}
onBlur={() => handlePricingInputBlur('min')}
/>
</InputGroup>
<span>-</span>
<InputGroup>
<InputGroup.Text>
<Eye size={16} />
</InputGroup.Text>
<Form.Control
type='number'
value={maxPricing}
onChange={handleMaxPricingChange}
onBlur={() => handlePricingInputBlur('max')}
/>
</InputGroup>
</div>
</div>
</div>
)}
</Card.Body> </Card.Body>
</div> </div>
); );

View File

@ -59,7 +59,26 @@ const menuItems = [
path: '/private-creators', path: '/private-creators',
icon: <Heart />, icon: <Heart />,
hasSubmenu: true, hasSubmenu: true,
submenuItems: [], submenuItems: [
{
id: 'tiktok',
title: 'TikTok',
path: '/private-creators/tiktok',
icon: <FontAwesomeIcon icon='fa-brands fa-tiktok' />,
},
{
id: 'instagram',
title: 'Instagram',
path: '/private-creators/instagram',
icon: <FontAwesomeIcon icon='fa-brands fa-instagram' />,
},
{
id: 'youtube',
title: 'YouTube',
path: '/private-creators/youtube',
icon: <FontAwesomeIcon icon='fa-brands fa-youtube' />,
},
],
}, },
{ {
id: 'deep-analysis', id: 'deep-analysis',

174
src/pages/CreatorDetail.jsx Normal file
View File

@ -0,0 +1,174 @@
import { ArrowLeft, Instagram, Link, Mail, MapPin } from 'lucide-react';
import { useEffect } from 'react';
import { Card } from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
import { selectCreator, clearCreator } from '../store/slices/creatorsSlice';
import { Doughnut } from 'react-chartjs-2';
const data = {
labels: ['Red', 'Blue', 'Green', 'Purple'],
datasets: [
{
label: 'GMV',
data: [12, 19, 5, 2],
backgroundColor: ['rgba(217, 107, 139)', 'rgba(101, 105, 225)', 'rgba(93, 200, 179)', 'rgba(122, 87, 218)'],
},
],
};
const options = {
cutout: 70,
plugins: {
legend: {
position: 'bottom',
labels: {
generateLabels: function (chart) {
return chart.data.labels.map((label, index) => {
const value = chart.data.datasets[0].data[index];
return {
text: `${label} ${value}%`,
fillStyle: chart.data.datasets[0].backgroundColor[index],
strokeStyle: chart.data.datasets[0].backgroundColor[index],
index: index,
};
});
},
},
},
},
};
export default function CreatorDetail({}) {
const { id } = useParams();
const navigate = useNavigate();
const dispatch = useDispatch();
const { selectedCreator } = useSelector((state) => state.creators);
const handleBack = () => {
navigate(-1);
};
useEffect(() => {
dispatch(selectCreator(id));
return () => {
dispatch(clearCreator());
};
}, [dispatch, id]);
console.log(selectedCreator);
return (
<div className='creator-detail-page'>
<div className='back-button' onClick={handleBack}>
<ArrowLeft size={16} /> Go back
</div>
{selectedCreator ? (
<div className='creator-info-detail-container'>
<div className='creator-info-container'>
<div className='creator-info-1'>
<div className='creator-avatar'>
<img src={selectedCreator.avatar} alt={selectedCreator.name} />
</div>
<div className='creator-info-right'>
<div className='creator-name'>{selectedCreator.name}</div>
<div className='creator-desc'>{selectedCreator.description || '--'}</div>
<div className='creator-category'>{selectedCreator.category || '--'}</div>
<div className='creator-location'>
<MapPin size={16} />
{selectedCreator.location || ' --'}
</div>
<div className='creator-live-time'>{selectedCreator.liveTime || '--'}</div>
</div>
</div>
<div className='creator-info-2'>
<div className='creator-info-item'>
<div className='creator-info-label'>Category</div>
<div className='creator-info-value'>{selectedCreator.category}</div>
</div>
<div className='creator-info-item'>
<div className='creator-info-label'>MCN</div>
<div className='creator-info-value'>{selectedCreator.mcn || '--'}</div>
</div>
<div className='creator-info-item'>
<div className='creator-info-label'>Pricing</div>
<div className='creator-info-value'>{selectedCreator.pricing}</div>
</div>
<div className='creator-info-item'>
<div className='creator-info-label'>Collab.</div>
<div className='creator-info-value'>{selectedCreator.collab || '--'}</div>
</div>
</div>
<div className='creator-info-3'>
<div className='creator-info-item'>
<div className='creator-info-label mail'>
<Mail size={18} />
</div>
<div className='creator-info-value'>{selectedCreator.email || '--'}</div>
</div>
<div className='creator-info-item'>
<div className='creator-info-label social'>
<Instagram size={18} />
</div>
<div className='creator-info-value'>{selectedCreator.instagram || '--'}</div>
</div>
<div className='creator-info-item'>
<div className='creator-info-label link'>
<Link size={18} />
</div>
<div className='creator-info-value'>{selectedCreator.url || '--'}</div>
</div>
</div>
</div>
<div className='creator-data'>
<div className='levels'>
<div className='level-item'>
<div className='name'>E-commerce Level</div>
<div className='value'>{selectedCreator.ecommerceLevel || '--'}</div>
</div>
<div className='level-item'>
<div className='name'>Exposure Level</div>
<div className='value'>{selectedCreator.exposureLevel || '--'}</div>
</div>
</div>
<div className='data-cards'>
<div className='data-card'>
<div className='value'>{selectedCreator.followers || '--'}</div>
<div className='name'>Followers</div>
</div>
<div className='data-card'>
<div className='value'>{selectedCreator.gmv || '--'}</div>
<div className='name'>GMV</div>
</div>
<div className='data-card'>
<div className='value'>{selectedCreator.avgVideoViews || '--'}</div>
<div className='name'>Avg Video Views</div>
</div>
<div className='data-card'>
<div className='value'>{selectedCreator.itemsSold || '--'}</div>
<div className='name'>Items Sold</div>
</div>
<div className='data-card'>
<div className='value'>{selectedCreator.gpm || '--'}</div>
<div className='name'>GPM</div>
</div>
<div className='data-card'>
<div className='value'>{selectedCreator.gpmPerCustomer || '--'}</div>
<div className='name'>GMV per customer</div>
</div>
</div>
<div className='data-charts'>
<div className='data-chart'>
<div className='chart-title'>GMV per sales channel</div>
<Doughnut data={data} options={options} />
</div>
<div className='data-chart'>
<div className='chart-title'>GMV by product category</div>
<Doughnut data={data} options={options} />
</div>
</div>
</div>
</div>
) : (
<div>No creator found</div>
)}
</div>
);
}

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import DatabaseFilter from '../components/DatabaseFilter'; import DatabaseFilter from '../components/DatabaseFilter';
import DatabaseList from '../components/DatabaseList'; import CreatorList from '../components/CreatorList';
import SearchBar from '../components/SearchBar'; import SearchBar from '../components/SearchBar';
import { Button } from 'react-bootstrap'; import { Button } from 'react-bootstrap';
export default function Database({ path }) { export default function Database({ path }) {
@ -16,8 +16,8 @@ export default function Database({ path }) {
{path === 'instagram' && <div className='breadcrumb-item'>Instagram</div>} {path === 'instagram' && <div className='breadcrumb-item'>Instagram</div>}
{path === 'youtube' && <div className='breadcrumb-item'>YouTube</div>} {path === 'youtube' && <div className='breadcrumb-item'>YouTube</div>}
</div> </div>
<DatabaseFilter path={path} /> <DatabaseFilter path={path} pageType={'database'} />
<DatabaseList path={path} /> <CreatorList path={path} pageType={'database'} />
</React.Fragment> </React.Fragment>
); );
} }

View File

@ -0,0 +1,25 @@
import React from 'react';
import SearchBar from '../components/SearchBar';
import { Button } from 'react-bootstrap';
import DatabaseFilter from '../components/DatabaseFilter';
import CreatorList from '../components/CreatorList';
export default function PrivateCreator({ path }) {
return (
<React.Fragment>
<div className='function-bar'>
<SearchBar />
<Button>+ Add to Campaign</Button>
</div>
<div className='breadcrumb'>
<div className='breadcrumb-item'>Private Creators</div>
{path === 'tiktok' && <div className='breadcrumb-item'>TikTok</div>}
{path === 'instagram' && <div className='breadcrumb-item'>Instagram</div>}
{path === 'youtube' && <div className='breadcrumb-item'>YouTube</div>}
</div>
<DatabaseFilter path={path} pageType={'private'} />
<CreatorList path={path} pageType={'private'} />
</React.Fragment>
);
}

View File

@ -9,6 +9,8 @@ import BrandsDetail from '@/pages/BrandsDetail';
import CampaignDetail from '@/pages/CampaignDetail'; import CampaignDetail from '@/pages/CampaignDetail';
import Login from '@/pages/Login'; import Login from '@/pages/Login';
import CreatorDiscovery from '@/pages/CreatorDiscovery'; import CreatorDiscovery from '@/pages/CreatorDiscovery';
import PrivateCreator from '../pages/PrivateCreator';
import CreatorDetail from '../pages/CreatorDetail';
// Routes configuration object // Routes configuration object
const routes = [ const routes = [
@ -25,7 +27,7 @@ const routes = [
children: [ children: [
{ {
path: '', path: '',
element: <Database />, element: <Database path='tiktok'/>,
}, },
{ {
path: 'tiktok', path: 'tiktok',
@ -42,8 +44,25 @@ const routes = [
], ],
}, },
{ {
path: '/private-creators/*', path: '/private-creators',
element: <Home />, children: [
{
path: '',
element: <PrivateCreator path='tiktok' />,
},
{
path: 'tiktok',
element: <PrivateCreator path='tiktok' />,
},
{
path: 'instagram',
element: <PrivateCreator path='instagram' />,
},
{
path: 'youtube',
element: <PrivateCreator path='youtube' />,
},
],
}, },
{ {
path: '/deep-analysis', path: '/deep-analysis',
@ -65,6 +84,10 @@ const routes = [
path: '/settings', path: '/settings',
element: <Home />, element: <Home />,
}, },
{
path: '/creator/:id',
element: <CreatorDetail />,
},
]; ];
// Create router with routes wrapped in the layout // Create router with routes wrapped in the layout

View File

@ -158,6 +158,7 @@ const initialState = {
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null, error: null,
selectedCreators: [], selectedCreators: [],
selectedCreator: null,
}; };
const creatorsSlice = createSlice({ const creatorsSlice = createSlice({
@ -169,7 +170,7 @@ const creatorsSlice = createSlice({
const isSelected = state.selectedCreators.includes(creatorId); const isSelected = state.selectedCreators.includes(creatorId);
if (isSelected) { if (isSelected) {
state.selectedCreators = state.selectedCreators.filter((id) => id !== creatorId); state.selectedCreators = state.selectedCreators.filter((id) => id.toString() !== creatorId.toString());
} else { } else {
state.selectedCreators.push(creatorId); state.selectedCreators.push(creatorId);
} }
@ -180,6 +181,13 @@ const creatorsSlice = createSlice({
clearCreatorSelection: (state) => { clearCreatorSelection: (state) => {
state.selectedCreators = []; state.selectedCreators = [];
}, },
selectCreator: (state, action) => {
const id = action.payload;
state.selectedCreator = state.creators.find((creator) => creator.id.toString() === id.toString());
},
clearCreator: (state) => {
state.selectedCreator = null;
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder builder
@ -198,6 +206,7 @@ const creatorsSlice = createSlice({
}, },
}); });
export const { toggleCreatorSelection, selectAllCreators, clearCreatorSelection } = creatorsSlice.actions; export const { toggleCreatorSelection, selectAllCreators, clearCreatorSelection, selectCreator, clearCreator } =
creatorsSlice.actions;
export default creatorsSlice.reducer; export default creatorsSlice.reducer;

View File

@ -6,10 +6,10 @@ const initialState = {
exposureRatings: [], exposureRatings: [],
gmvRanges: ['$5k - $25k', '$25k - $60k'], gmvRanges: ['$5k - $25k', '$25k - $60k'],
viewsRange: [0, 100000], viewsRange: [0, 100000],
pricingRange: [0, 3000],
sortBy: 'followers', sortBy: 'followers',
sortDirection: 'desc', sortDirection: 'desc',
}; };
const filtersSlice = createSlice({ const filtersSlice = createSlice({
name: 'filters', name: 'filters',
initialState, initialState,
@ -49,6 +49,9 @@ const filtersSlice = createSlice({
setViewsRange: (state, action) => { setViewsRange: (state, action) => {
state.viewsRange = action.payload; state.viewsRange = action.payload;
}, },
setPricingRange: (state, action) => {
state.pricingRange = action.payload;
},
setSortBy: (state, action) => { setSortBy: (state, action) => {
// 如果选择了当前已激活的排序项,则切换排序方向 // 如果选择了当前已激活的排序项,则切换排序方向
if (state.sortBy === action.payload) { if (state.sortBy === action.payload) {
@ -71,6 +74,7 @@ export const {
toggleExposureRating, toggleExposureRating,
toggleGmvRange, toggleGmvRange,
setViewsRange, setViewsRange,
setPricingRange,
setSortBy, setSortBy,
resetFilters, resetFilters,
} = filtersSlice.actions; } = filtersSlice.actions;

View File

@ -51,3 +51,168 @@
border: 1px solid #171a1f12; border: 1px solid #171a1f12;
} }
} }
.creator-detail-page {
.creator-info-detail-container {
display: flex;
flex-direction: row;
gap: 1rem;
justify-content: space-between;
.creator-info-container {
width: 40%;
display: flex;
flex-flow: column nowrap;
background: #ffffffff; /* white */
border-radius: 8px; /* border-l */
box-shadow: 0px 0px 1px #171a1f12, 0px 0px 2px #171a1f1f; /* shadow-xs */
gap: 1rem;
padding: 1rem;
.creator-info-1 {
display: flex;
flex-flow: row nowrap;
gap: 1rem;
align-items: center;
border-bottom: 2px solid $neutral-300;
padding-bottom: 1rem;
.creator-avatar {
width: 115px;
height: 115px;
border-radius: 50%;
overflow: hidden;
}
.creator-info-right {
display: flex;
flex-flow: column nowrap;
gap: 0.5rem;
.creator-name {
font-size: 1.5rem;
font-weight: 700;
color: $primary;
}
}
}
.creator-info-2 {
display: flex;
flex-flow: column nowrap;
gap: 1rem;
border-bottom: 2px solid $neutral-300;
padding-bottom: 1rem;
.creator-info-item {
display: flex;
flex-flow: row nowrap;
color: $neutral-900;
.creator-info-label {
font-weight: 700;
width: 90px;
}
.creator-info-value {
}
}
}
.creator-info-3 {
display: flex;
flex-flow: column nowrap;
gap: 1rem;
.creator-info-item {
display: flex;
flex-flow: row nowrap;
color: $neutral-900;
gap: 0.5rem;
.creator-info-label {
font-weight: 700;
padding: 0.45rem;
border-radius: 50%;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
&.mail {
background-color: $primary-500;
}
&.social {
background-color: $secondary-500;
}
&.link {
background-color: $info-500;
}
}
}
}
}
.creator-data {
width: 65%;
background: #ffffffff; /* white */
border-radius: 8px; /* border-l */
box-shadow: 0px 0px 1px #171a1f12, 0px 0px 2px #171a1f1f; /* shadow-xs */
padding: 1rem;
display: flex;
flex-flow: column nowrap;
gap: 1rem;
.levels {
display: flex;
flex-flow: row nowrap;
.level-item {
flex: 1;
text-align: start;
display: flex;
flex-flow: row nowrap;
gap: 1rem;
.name {
font-weight: 700;
}
.value {
border-radius: 1rem;
background-color: $primary-100;
color: $primary;
font-size: .75rem;
line-height: 1.5;
padding: 0.25rem 0.5rem;
}
}
}
.data-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: .5rem;
.data-card {
display: flex;
flex-flow: column nowrap;
align-items: center;
justify-content: center;
background-color: $neutral-150;
border-radius: 6px;
padding: 12px 0;
.value {
font-weight: 700;
line-height: 1.5rem;
}
.name {
color: $neutral-500;
}
}
}
.data-charts {
display: flex;
flex-flow: row nowrap;
.data-chart {
flex: 1;
.chart-title {
font-weight: 700;
color: $neutral-900;
margin-bottom: 1rem;
}
canvas {
max-width: 260px;
max-height: 260px;
}
}
}
}
}
}

View File

@ -29,7 +29,7 @@
gap: 0.5rem; gap: 0.5rem;
flex: 1; flex: 1;
&.filter-views { &.filter-range-slider {
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
gap: 1rem; gap: 1rem;

View File

@ -36,7 +36,8 @@
} }
.creator-name { .creator-name {
font-weight: 500; font-weight: 700;
color: #090909
} }
} }

View File

@ -44,7 +44,7 @@
position: absolute; position: absolute;
height: 5px; height: 5px;
border-radius: 3px; border-radius: 3px;
background-color: $indigo-500; background-color: $primary;
} }
&__steps { &__steps {
@ -65,8 +65,8 @@
transition: all 0.2s ease; transition: all 0.2s ease;
&.active { &.active {
background-color: $indigo-500; background-color: $primary;
border-color: $indigo-500; border-color: $primary;
transform: scale(1.2); transform: scale(1.2);
} }
} }
@ -83,7 +83,7 @@
transform: translateX(-50%); transform: translateX(-50%);
font-size: 0.75rem; font-size: 0.75rem;
color: white; color: white;
background-color: $indigo-500; background-color: $primary;
padding: 2px 6px; padding: 2px 6px;
border-radius: 10px; border-radius: 10px;
white-space: nowrap; white-space: nowrap;
@ -100,7 +100,7 @@
height: 0; height: 0;
border-left: 5px solid transparent; border-left: 5px solid transparent;
border-right: 5px solid transparent; border-right: 5px solid transparent;
border-bottom: 5px solid $indigo-500; border-bottom: 5px solid $primary;
} }
} }
} }
@ -123,13 +123,13 @@
height: 20px; height: 20px;
border-radius: 50%; border-radius: 50%;
background-color: white; background-color: white;
border: 2px solid $indigo-500; border: 2px solid $primary;
cursor: pointer; cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover { &:hover {
background-color: $indigo-500; background-color: $primary;
transform: scale(1.1); transform: scale(1.1);
} }
@ -146,13 +146,13 @@
height: 20px; height: 20px;
border-radius: 50%; border-radius: 50%;
background-color: white; background-color: white;
border: 2px solid $indigo-500; border: 2px solid $primary;
cursor: pointer; cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover { &:hover {
background-color: $indigo-500; background-color: $primary;
transform: scale(1.1); transform: scale(1.1);
} }
@ -190,7 +190,7 @@
/* For Chrome browsers */ /* For Chrome browsers */
.thumb::-webkit-slider-thumb { .thumb::-webkit-slider-thumb {
background-color: #fff; background-color: #fff;
border: 2px solid $indigo-500; border: 2px solid $primary;
border-radius: 50%; border-radius: 50%;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); box-shadow: 0 0 3px rgba(0, 0, 0, 0.2);
cursor: pointer; cursor: pointer;
@ -204,7 +204,7 @@
/* For Firefox browsers */ /* For Firefox browsers */
.thumb::-moz-range-thumb { .thumb::-moz-range-thumb {
background-color: #fff; background-color: #fff;
border: 2px solid $indigo-500; border: 2px solid $primary;
border-radius: 50%; border-radius: 50%;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); box-shadow: 0 0 3px rgba(0, 0, 0, 0.2);
cursor: pointer; cursor: pointer;

View File

@ -1,26 +1,183 @@
// 主题颜色变量 // 主题颜色变量
$primary: #636AE8FF; // 靛蓝色
$secondary: #6c757d; // 灰色
$success: #198754; // 绿色
$info: #0dcaf0; // 浅蓝色
$warning: #ffc107; // 黄色
$danger: #dc3545; // 红色
$light: #f8f9fa; // 浅色 $light: #f8f9fa; // 浅色
$dark: #212529; // 深色 $dark: #212529; // 深色
// 自定义颜色变量
$primary-100: #F2F2FDFF; $primary-100: #F2F2FDFF;
$primary-150: #E0E1FAFF; $primary-150: #E0E1FAFF;
$primary-200: #CED0F8FF;
$primary-250: #BCBFF5FF;
$primary-300: #ABAEF2FF;
$primary-350: #999DF0FF;
$primary-400: #878CEDFF;
$primary-450: #757BEAFF;
$primary-500: #636AE8FF; $primary-500: #636AE8FF;
$indigo-50: #eef2ff; $primary-550: #4850E4FF;
$indigo-100: #e0e7ff; $primary-600: #2C35E0FF;
$indigo-500: #6366f1; $primary-650: #1F27CDFF;
$violet-50: #f5f3ff; $primary-700: #1B22B1FF;
$violet-100: #ede9fe; $primary-750: #161D96FF;
$violet-400: #a78bfa; $primary-800: #12177AFF;
$primary-850: #0E125EFF;
$primary-900: #0A0D42FF;
$primary: #636AE8FF;
$secondary-100: #FDF1F5FF;
$secondary-150: #FBE0E8FF;
$secondary-200: #F8CEDBFF;
$secondary-250: #F5BCCEFF;
$secondary-300: #F3AAC1FF;
$secondary-350: #F098B4FF;
$secondary-400: #EE86A7FF;
$secondary-450: #EB759AFF;
$secondary-500: #E8618CFF;
$secondary-550: #E44578FF;
$secondary-600: #E02862FF;
$secondary-650: #C91D53FF;
$secondary-700: #AC1947FF;
$secondary-750: #8E143BFF;
$secondary-800: #71102FFF;
$secondary-850: #530C22FF;
$secondary-900: #360816FF;
$secondary: #E8618CFF;
$info-100: #F1F8FDFF;
$info-150: #DAECFAFF;
$info-200: #C3E1F8FF;
$info-250: #ACD5F5FF;
$info-300: #94C9F2FF;
$info-350: #7DBEEFFF;
$info-400: #66B2ECFF;
$info-450: #4FA6E9FF;
$info-500: #379AE6FF;
$info-550: #1D8DE3FF;
$info-600: #197DCAFF;
$info-650: #166DB0FF;
$info-700: #125D95FF;
$info-750: #0F4C7BFF;
$info-800: #0C3C61FF;
$info-850: #092C47FF;
$info-900: #061C2DFF;
$info: #379AE6FF;
$warning-100: #FEF9EEFF;
$warning-150: #FCF0D7FF;
$warning-200: #FAE7C0FF;
$warning-250: #F8DEA9FF;
$warning-300: #F6D491FF;
$warning-350: #F4CB7AFF;
$warning-400: #F2C263FF;
$warning-450: #F0B94BFF;
$warning-500: #EFB034FF;
$warning-550: #ECA517FF;
$warning-600: #D29211FF;
$warning-650: #B57E0FFF;
$warning-700: #98690CFF;
$warning-750: #7A550AFF;
$warning-800: #5D4108FF;
$warning-850: #402C05FF;
$warning-900: #221803FF;
$warning: #EFB034FF;
$danger-100: #FDF2F2FF;
$danger-150: #F9DBDCFF;
$danger-200: #F5C4C6FF;
$danger-250: #F1ADAF;
$danger-300: #ED9699FF;
$danger-350: #E97F83FF;
$danger-400: #E5696DFF;
$danger-450: #E25256FF;
$danger-500: #DE3B40FF;
$danger-550: #D9252BFF;
$danger-600: #C12126FF;
$danger-650: #AA1D22FF;
$danger-700: #93191DFF;
$danger-750: #7B1518FF;
$danger-800: #641114FF;
$danger-850: #4D0D0FFF;
$danger-900: #36090BFF;
$danger: #DE3B40FF;
$success-100: #EEFDF3FF;
$success-150: #D3F9E0FF;
$success-200: #B8F5CDFF;
$success-250: #9DF2B9FF;
$success-300: #82EEA6FF;
$success-350: #67EA93FF;
$success-400: #4CE77FFF;
$success-450: #31E36CFF;
$success-500: #1DD75BFF;
$success-550: #1AC052FF;
$success-600: #17A948FF;
$success-650: #14923EFF;
$success-700: #117B34FF;
$success-750: #0E642AFF;
$success-800: #0A4D20FF;
$success-850: #073517FF;
$success-900: #041E0DFF;
$success: #1DD75BFF;
$color-3-100: #EFFCFAFF;
$color-3-150: #D4F8F2FF;
$color-3-200: #BAF3EBFF;
$color-3-250: #9FEFE3FF;
$color-3-300: #84EADBFF;
$color-3-350: #69E6D3FF;
$color-3-400: #4EE1CBFF;
$color-3-450: #33DCC3FF;
$color-3-500: #22CCB2FF;
$color-3-550: #1FB7A0FF;
$color-3-600: #1BA18DFF;
$color-3-650: #188B7AFF;
$color-3-700: #147567FF;
$color-3-750: #105F53FF;
$color-3-800: #0C4940FF;
$color-3-850: #09332DFF;
$color-3-900: #051D1AFF;
$color-3: #22CCB2FF;
$color-4-100: #F5F2FDFF;
$color-4-150: #E7DFF9FF;
$color-4-200: #D8CBF5FF;
$color-4-250: #C9B8F2FF;
$color-4-300: #BBA4EEFF;
$color-4-350: #AC91EBFF;
$color-4-400: #9D7EE7FF;
$color-4-450: #8F6AE4FF;
$color-4-500: #7F55E0FF;
$color-4-550: #6D3EDCFF;
$color-4-600: #5B27D5FF;
$color-4-650: #5123BCFF;
$color-4-700: #461EA4FF;
$color-4-750: #3B198BFF;
$color-4-800: #311572FF;
$color-4-850: #261059FF;
$color-4-900: #1C0C40FF;
$color-4: #7F55E0FF;
$color-5-100: #FDF5F1FF;
$color-5-150: #FBE8E1FF;
$color-5-200: #F8DBD0FF;
$color-5-250: #F6CFBFFF;
$color-5-300: #F4C2AFFF;
$color-5-350: #F1B59EFF;
$color-5-400: #EFA98DFF;
$color-5-450: #EC9C7CFF;
$color-5-500: #EA916EFF;
$color-5-550: #E5784CFF;
$color-5-600: #E1602CFF;
$color-5-650: #CC4F1DFF;
$color-5-700: #AC4219FF;
$color-5-750: #8D3614FF;
$color-5-800: #6D2A10FF;
$color-5-850: #4D1E0BFF;
$color-5-900: #2D1206FF;
$color-5: #EA916EFF;
$neutral-150: #f8f9faff; $neutral-150: #f8f9faff;
$neutral-200: #f3f4f6ff; $neutral-200: #f3f4f6ff;
$neutral-300: #DEE1E6FF; /* neutral-300 */;
$neutral-350: #cfd2daff; $neutral-350: #cfd2daff;
$neutral-500: #9095A0FF;
$neutral-600: #565e6cff; $neutral-600: #565e6cff;
$neutral-700: #323842ff; $neutral-700: #323842ff;
$neutral-900: #171a1fff; $neutral-900: #171a1fff;

View File

@ -55,7 +55,7 @@ a {
.table { .table {
border-collapse: separate; border-collapse: separate;
border-spacing: 0; border-spacing: 0;
border: 1px solid #171a1f1f;
th { th {
background-color: #f8f9fa; background-color: #f8f9fa;
border-top: none; border-top: none;

View File

@ -24,3 +24,7 @@
gap: 0.25rem; gap: 0.25rem;
} }
} }
.back-button {
cursor: pointer;
}