diff --git a/.env b/.env
new file mode 100644
index 0000000..ee02390
--- /dev/null
+++ b/.env
@@ -0,0 +1,2 @@
+VITE_PROD = false
+VITE_API_URL = "http://81.69.223.133:58099"
\ No newline at end of file
diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..b5ea5b7
--- /dev/null
+++ b/.env.development
@@ -0,0 +1,2 @@
+VITE_PROD = false
+VITE_API_URL = "http://81.69.223.133:58099"
diff --git a/.env.production b/.env.production
new file mode 100644
index 0000000..ee02390
--- /dev/null
+++ b/.env.production
@@ -0,0 +1,2 @@
+VITE_PROD = false
+VITE_API_URL = "http://81.69.223.133:58099"
\ No newline at end of file
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index d155639..a9af679 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -81,12 +81,12 @@ OOIN Creator Center is a React application built with Vite that allows users to
name: string,
avatar: string,
category: string,
- ecommerceLevel: string,
- exposureLevel: string,
+ e_commerce_level: string,
+ exposure_level: string,
followers: string,
gmv: string,
soldPercentage: string,
- avgViews: string,
+ avg_video_views: string,
hasEcommerce: boolean,
hasTiktok: boolean,
hasInstagram: boolean, // optional
diff --git a/package-lock.json b/package-lock.json
index 7db4de0..0108d7e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,8 @@
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@reduxjs/toolkit": "^2.8.1",
+ "@tanstack/react-table": "^8.21.3",
+ "axios": "^1.9.0",
"bootstrap": "^5.3.3",
"chart.js": "^4.4.9",
"date-fns": "^4.1.0",
@@ -1771,6 +1773,37 @@
"tslib": "^2.8.0"
}
},
+ "node_modules/@tanstack/react-table": {
+ "version": "8.21.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
+ "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
+ "dependencies": {
+ "@tanstack/table-core": "8.21.3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/@tanstack/table-core": {
+ "version": "8.21.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
+ "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1963,6 +1996,11 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
"node_modules/attr-accept": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
@@ -1971,6 +2009,16 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
+ "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2089,7 +2137,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
@@ -2208,6 +2255,17 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -2329,6 +2387,14 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -2370,7 +2436,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "dev": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
@@ -2405,7 +2470,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "dev": true,
"engines": {
"node": ">= 0.4"
}
@@ -2414,7 +2478,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
"engines": {
"node": ">= 0.4"
}
@@ -2423,7 +2486,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "dev": true,
"dependencies": {
"es-errors": "^1.3.0"
},
@@ -2431,6 +2493,20 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/esbuild": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
@@ -2875,6 +2951,58 @@
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
+ "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/form-data/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/form-data/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -2911,7 +3039,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -2929,7 +3056,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "dev": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
@@ -2953,7 +3079,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "dev": true,
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
@@ -2990,7 +3115,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "dev": true,
"engines": {
"node": ">= 0.4"
},
@@ -3011,7 +3135,20 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
"engines": {
"node": ">= 0.4"
},
@@ -3023,7 +3160,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -3564,7 +3700,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "dev": true,
"engines": {
"node": ">= 0.4"
}
@@ -3937,6 +4072,11 @@
"node": ">= 0.10"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
diff --git a/package.json b/package.json
index f62ac6c..e52f5f3 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,8 @@
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@reduxjs/toolkit": "^2.8.1",
+ "@tanstack/react-table": "^8.21.3",
+ "axios": "^1.9.0",
"bootstrap": "^5.3.3",
"chart.js": "^4.4.9",
"date-fns": "^4.1.0",
diff --git a/src/assets/placeholder.png b/src/assets/placeholder.png
new file mode 100644
index 0000000..444f317
Binary files /dev/null and b/src/assets/placeholder.png differ
diff --git a/src/components/ChatDetails.jsx b/src/components/ChatDetails.jsx
index d992712..379ca82 100644
--- a/src/components/ChatDetails.jsx
+++ b/src/components/ChatDetails.jsx
@@ -1,7 +1,7 @@
import { Send, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
-import { selectCreator } from '../store/slices/creatorsSlice';
+import { fetchCreatorDetail } from '../store/slices/creatorsSlice';
import { Button, Form } from 'react-bootstrap';
export default function ChatDetails({ onCloseChatDetails }) {
@@ -16,7 +16,7 @@ export default function ChatDetails({ onCloseChatDetails }) {
};
useEffect(() => {
- dispatch(selectCreator(selectedChat.id));
+ dispatch(fetchCreatorDetail({ creatorId: selectedChat.id }));
}, [dispatch, selectedChat]);
return (
diff --git a/src/components/CreatorList.jsx b/src/components/CreatorList.jsx
index e6544f3..5903b62 100644
--- a/src/components/CreatorList.jsx
+++ b/src/components/CreatorList.jsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+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';
@@ -7,27 +7,50 @@ import {
toggleCreatorSelection,
selectAllCreators,
clearCreatorSelection,
+ fetchPrivateCreators,
+ setCreators,
+ resetCreators,
} 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 CreatorList({ path, pageType = 'database' }) {
+export default function CreatorList({ path }) {
const dispatch = useDispatch();
- const { creators, status, selectedCreators } = useSelector((state) => state.creators);
+ const {
+ publicCreators = [],
+ status,
+ selectedCreators = [],
+ hasMore,
+ isLoadingMore,
+ pagination,
+ } = 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(fetchCreators({ path, page: nextPage }));
+ }
+ });
+ if (node) observer.current.observe(node);
+ },
+ [isLoadingMore, hasMore, pagination, path, dispatch]
+ );
// 组件加载时获取创作者数据
useEffect(() => {
- if (status === 'idle') {
- dispatch(fetchCreators({ path }));
- }
- }, [dispatch, status]);
+ dispatch(fetchCreators({ path, page: 1 }));
+ }, [path, dispatch]);
useEffect(() => {
- // console.log(creators);
- }, [creators]);
+ }, [publicCreators]);
+
// 处理全选/取消全选
const handleSelectAll = (e) => {
if (e.target.checked) {
@@ -69,8 +92,8 @@ export default function CreatorList({ path, pageType = 'database' }) {
return categoryMap[category] || '';
};
- // 如果正在加载,显示加载中
- if (status === 'loading') {
+ // 如果正在加载且没有数据,显示加载中
+ if (status === 'loading' && (!publicCreators || publicCreators.length === 0)) {
return (
@@ -87,121 +110,122 @@ export default function CreatorList({ path, pageType = 'database' }) {
return (
-
-
-
-
- 0}
- onChange={handleSelectAll}
- />
- |
- handleSort('name')} style={{ width: '180px' }}>
- Creator {renderSortIcon('name')}
- |
- handleSort('category')}
- style={{ width: '180px' }}
- >
- Category {renderSortIcon('category')}
- |
- handleSort('ecommerceLevel')}>
- E-commerce Level {renderSortIcon('ecommerceLevel')}
- |
- handleSort('exposureLevel')}>
- Exposure Level {renderSortIcon('exposureLevel')}
- |
- handleSort('followers')}>
- Followers {renderSortIcon('followers')}
- |
- handleSort('gmv')}>
- GMV {renderSortIcon('gmv')}
- |
- handleSort('avgViews')}>
- Avg. Video Views {renderSortIcon('avgViews')}
- |
- {pageType === 'private' && (
- <>
- Pricing |
- # Collab |
- Latest Collab. |
- >
- )}
- E-commerce |
- Profile |
-
-
-
- {creators.length === 0 ? (
+
+
+
-
- No creators found matching your filters.
- |
+
+ 0}
+ onChange={handleSelectAll}
+ />
+ |
+ handleSort('name')} style={{ width: '180px' }}>
+ Creator {renderSortIcon('name')}
+ |
+ handleSort('category')}
+ style={{ width: '180px' }}
+ >
+ Category {renderSortIcon('category')}
+ |
+ handleSort('e_commerce_level')}>
+ E-commerce Level {renderSortIcon('e_commerce_level')}
+ |
+ handleSort('exposure_level')}>
+ Exposure Level {renderSortIcon('exposure_level')}
+ |
+ handleSort('followers')}>
+ Followers {renderSortIcon('followers')}
+ |
+ handleSort('gmv')}>
+ GMV {renderSortIcon('gmv')}
+ |
+ handleSort('avg_video_views')}>
+ Avg. Video Views {renderSortIcon('avg_video_views')}
+ |
+ E-commerce |
+ Profile |
- ) : (
- creators.map((creator) => (
-
-
- handleSelectCreator(creator.id)}
- />
- |
-
-
-
- 
- {creator.verified && }
-
-
- {creator.name}
-
-
- |
-
-
- {creator.category}
-
- |
-
- {creator.ecommerceLevel}
- |
-
-
- {creator.exposureLevel}
-
- |
- {creator.followers} |
-
- {creator.gmv}
- Items Sold: {creator.soldPercentage}
- |
- {creator.avgViews} |
- {pageType === 'private' && (
- <>
- {creator.pricing} |
- {creator.collabCount} |
- {creator.latestCollab} |
- >
- )}
-
- {creator.hasEcommerce ? : null}
- |
-
- {creator.hasTiktok && (
-
-
-
- )}
+ |
+
+ {!publicCreators || publicCreators.length <= 0 ? (
+
+
+ No creators found matching your filters.
|
- ))
- )}
-
-
+ ) : (
+ publicCreators.map((creator) => (
+
+
+ handleSelectCreator(creator.public_id)}
+ />
+ |
+
+
+
+ 
+ {creator.status && }
+
+
+ {creator.name}
+
+
+ |
+
+
+ {creator.category}
+
+ |
+
+ {creator.e_commerce_level}
+ |
+
+
+ {creator.exposure_level}
+
+ |
+ {creator.followers} |
+
+ {creator.gmv}
+ Items Sold: {creator.soldPercentage}
+ |
+ {creator.avg_video_views} |
+
+ {creator.hasEcommerce ? : null}
+ |
+
+ {creator.hasTiktok && (
+
+
+
+ )}
+ |
+
+ ))
+ )}
+
+
+ {hasMore && (
+
+
+ Loading more...
+
+
+ )}
+
);
}
diff --git a/src/components/DatabaseFilter.jsx b/src/components/DatabaseFilter.jsx
index a0f4df5..02e8578 100644
--- a/src/components/DatabaseFilter.jsx
+++ b/src/components/DatabaseFilter.jsx
@@ -78,8 +78,8 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
// 组件加载时获取数据
useEffect(() => {
- dispatch(fetchCreators({ path }));
- }, [dispatch, filters]);
+
+ }, [dispatch, filters, pageType, path]);
// 处理类别选择
const handleCategorySelect = (category) => {
diff --git a/src/components/PrivateCreatorList.jsx b/src/components/PrivateCreatorList.jsx
new file mode 100644
index 0000000..534822f
--- /dev/null
+++ b/src/components/PrivateCreatorList.jsx
@@ -0,0 +1,230 @@
+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 } = 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());
+ } else {
+ dispatch(clearCreatorSelection());
+ }
+ };
+
+ // 处理单个创作者选择
+ const handleSelectCreator = (creatorId) => {
+ dispatch(toggleCreatorSelection(creatorId));
+ };
+
+ // 处理排序
+ const handleSort = (field) => {
+ dispatch(setSortBy(field));
+ };
+
+ // 渲染排序图标
+ const renderSortIcon = (field) => {
+ if (sortBy === field) {
+ return sortDirection === 'asc' ? : ;
+ }
+ 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' && (!privateCreators || privateCreators.length === 0)) {
+ return (
+
+
+ Loading...
+
+
+ );
+ }
+
+ // 如果加载失败,显示错误信息
+ if (status === 'failed') {
+ return Failed to load creators. Please try again later.
;
+ }
+
+ return (
+
+
+
+
+
+
+ 0}
+ onChange={handleSelectAll}
+ />
+ |
+ handleSort('name')} style={{ width: '180px' }}>
+ Creator {renderSortIcon('name')}
+ |
+ handleSort('category')}
+ style={{ width: '180px' }}
+ >
+ Category {renderSortIcon('category')}
+ |
+ handleSort('e_commerce_level')}>
+ E-commerce Level {renderSortIcon('e_commerce_level')}
+ |
+ handleSort('exposure_level')}>
+ Exposure Level {renderSortIcon('exposure_level')}
+ |
+ handleSort('followers')}>
+ Followers {renderSortIcon('followers')}
+ |
+ handleSort('gmv')}>
+ GMV {renderSortIcon('gmv')}
+ |
+ handleSort('avg_video_views')}>
+ Avg. Video Views {renderSortIcon('avg_video_views')}
+ |
+ Pricing |
+ # Collab |
+ Latest Collab. |
+ E-commerce |
+ Profile |
+
+
+
+ {!privateCreators || privateCreators.length <= 0 ? (
+
+
+ No creators found matching your filters.
+ |
+
+ ) : (
+ privateCreators.map((creator) => (
+
+
+ handleSelectCreator(creator.creator_id)}
+ />
+ |
+
+
+
+ 
+ {creator.status && }
+
+
+ {creator.name}
+
+
+ |
+
+
+ {creator.category}
+
+ |
+
+ {creator.e_commerce_level}
+ |
+
+
+ {creator.exposure_level}
+
+ |
+ {creator.followers} |
+
+ {creator.gmv}
+ Items Sold: {creator.soldPercentage}
+ |
+ {creator.avg_video_views} |
+ {creator.pricing} |
+ {creator.collab_count} |
+ {creator.latestCollab} |
+
+ {creator.hasEcommerce ? : null}
+ |
+
+ {creator.hasTiktok && (
+
+
+
+ )}
+ |
+
+ ))
+ )}
+
+
+ {hasMore && (
+
+
+ Loading more...
+
+
+ )}
+
+
+ );
+}
diff --git a/src/main.jsx b/src/main.jsx
index e8014f9..9e48fba 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -1,4 +1,3 @@
-import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './store';
@@ -7,9 +6,7 @@ import './index.css';
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
-
-
-
-
-
+
+
+
);
diff --git a/src/pages/CreatorDetail.jsx b/src/pages/CreatorDetail.jsx
index e8671d3..8f73578 100644
--- a/src/pages/CreatorDetail.jsx
+++ b/src/pages/CreatorDetail.jsx
@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
import { Card, Table } from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
-import { selectCreator, clearCreator } from '../store/slices/creatorsSlice';
+import { clearCreator, fetchCreatorDetail } from '../store/slices/creatorsSlice';
import { Bar, Doughnut } from 'react-chartjs-2';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -57,19 +57,67 @@ export default function CreatorDetail({}) {
const { id } = useParams();
const navigate = useNavigate();
const dispatch = useDispatch();
- const { selectedCreator } = useSelector((state) => state.creators);
+ const { selectedCreator, status } = useSelector((state) => state.creators);
const [activeTab, setActiveTab] = useState('basic');
const handleBack = () => {
navigate(-1);
};
+
useEffect(() => {
- dispatch(selectCreator(id));
+ dispatch(fetchCreatorDetail({ creatorId: id }));
return () => {
dispatch(clearCreator());
};
}, [dispatch, id]);
- console.log(selectedCreator);
+
+ const processChartData = (data, chart) => {
+ switch (chart) {
+ case 'channel':
+ return {
+ labels: Object.keys(data),
+ datasets: [
+ {
+ label: 'GMV',
+ data: Object.values(data),
+ backgroundColor: [
+ 'rgba(217, 107, 139)',
+ 'rgba(101, 105, 225)',
+ 'rgba(93, 200, 179)',
+ 'rgba(122, 87, 218)',
+ ],
+ },
+ ],
+ };
+ case 'category':
+ return {
+ labels: Object.keys(data),
+ datasets: [
+ {
+ label: 'Sold',
+ data: Object.values(data),
+ backgroundColor: [
+ 'rgba(217, 107, 139)',
+ 'rgba(101, 105, 225)',
+ 'rgba(93, 200, 179)',
+ 'rgba(122, 87, 218)',
+ ],
+ },
+ ],
+ };
+ }
+ };
+ if (status === 'loading') {
+ return Loading...
;
+ }
+ if (status === 'failed') {
+ return Failed to load creator detail
;
+ }
+
+ if (!selectedCreator) {
+ return No creator found
;
+ }
+
return (
@@ -80,35 +128,50 @@ export default function CreatorDetail({}) {
-

+
-
{selectedCreator.name}
-
{selectedCreator.description || '--'}
-
{selectedCreator.category || '--'}
+
{selectedCreator.creator.name}
+
{selectedCreator?.description || '--'}
+
{selectedCreator.business?.category || '--'}
- {selectedCreator.location || ' --'}
+ {selectedCreator.creator.location || ' --'}
-
{selectedCreator.liveTime || '--'}
+
{selectedCreator.live_schedule || '--'}
-
+
Category
-
{selectedCreator.category}
+
+ {selectedCreator.business?.categories.map((item, index) => (
+
+ {item}
+
+ ))}
+
MCN
-
{selectedCreator.mcn || '--'}
+
{selectedCreator.business?.mcn || '--'}
Pricing
-
{selectedCreator.pricing}
+
+ {selectedCreator.business?.pricing?.range || '--'}
+
-
+
Collab.
-
{selectedCreator.collab || '--'}
+
+
+ {selectedCreator.business?.collab_count || '--'}
+
+
+ {selectedCreator.business?.latest_collab || '--'}
+
+
@@ -116,19 +179,23 @@ export default function CreatorDetail({}) {
-
{selectedCreator.email || '--'}
+
{selectedCreator.creator.email || '--'}
-
{selectedCreator.instagram || '--'}
+
+ {selectedCreator.creator.social_accounts?.instagram || '--'}
+
-
{selectedCreator.url || '--'}
+
+ {selectedCreator.creator.social_accounts?.tiktok || '--'}
+
@@ -136,47 +203,53 @@ export default function CreatorDetail({}) {
E-commerce Level
-
{selectedCreator.ecommerceLevel || '--'}
+
{selectedCreator?.metrics?.e_commerce_level || '--'}
Exposure Level
-
{selectedCreator.exposureLevel || '--'}
+
{selectedCreator?.metrics?.exposure_level || '--'}
-
{selectedCreator.followers || '--'}
+
{selectedCreator?.metrics?.followers || '--'}
Followers
-
{selectedCreator.gmv || '--'}
+
{selectedCreator?.metrics?.gmv || '--'}
GMV
-
{selectedCreator.avgVideoViews || '--'}
+
{selectedCreator?.metrics?.avg_video_views || '--'}
Avg Video Views
-
{selectedCreator.itemsSold || '--'}
+
{selectedCreator?.metrics?.items_sold || '--'}
Items Sold
-
{selectedCreator.gpm || '--'}
+
{selectedCreator?.metrics?.gpm || '--'}
GPM
-
{selectedCreator.gpmPerCustomer || '--'}
+
{selectedCreator?.metrics?.gmv_per_customer || '--'}
GMV per customer
GMV per sales channel
-
+
GMV by product category
-
+
@@ -262,134 +335,134 @@ function CreatorBasicInfo({ selectedCreator }) {
Collaboration Metrics
-
{selectedCreator.avgCommissionRate || '--'}
+
{selectedCreator?.avg_commission_rate || '--'}
Avg. Commission Rate
-
{selectedCreator.products || '--'}
+
{selectedCreator?.products || '--'}
Products
-
{selectedCreator.brandCollaborations || '--'}
+
{selectedCreator?.brand_collaborations || '--'}
Brand Collaborations
-
{selectedCreator.productPrice || '--'}
+
{selectedCreator?.product_price || '--'}
Product Price
- Video{selectedCreator.videoTimeRange || '--'}
+ Video{selectedCreator?.videoTimeRange || '--'}
-
{selectedCreator.avgVideoGpm || '--'}
+
{selectedCreator?.avgVideoGpm || '--'}
Video GPM
-
{selectedCreator.videos.length || '--'}
+
{selectedCreator?.videos?.length || '--'}
Videos
-
{selectedCreator.avgVideoViews || '--'}
+
{selectedCreator?.avgVideoViews || '--'}
Avg. Video Views
-
{selectedCreator.avgVideoEngagements || '--'}
+
{selectedCreator?.avgVideoEngagements || '--'}
Avg. Video Engagement
-
{selectedCreator.avgVideoLikes || '--'}
+
{selectedCreator?.avgVideoLikes || '--'}
Avg. Video Likes
- Shoppable Video{selectedCreator.videoTimeRange || '--'}
+ Shoppable Video{selectedCreator?.videoTimeRange || '--'}
-
{selectedCreator.avgVideoGpm || '--'}
+
{selectedCreator?.avgVideoGpm || '--'}
Video GPM
-
{selectedCreator.videos.length || '--'}
+
{selectedCreator?.videos?.length || '--'}
Videos
-
{selectedCreator.avgVideoViews || '--'}
+
{selectedCreator?.avgVideoViews || '--'}
Avg. Video Views
-
{selectedCreator.avgVideoEngagements || '--'}
+
{selectedCreator?.avgVideoEngagements || '--'}
Avg. Video Engagement
-
{selectedCreator.avgVideoLikes || '--'}
+
{selectedCreator?.avgVideoLikes || '--'}
Avg. Video Likes
- LIVE{selectedCreator.liveTimeRange || '--'}
+ LIVE{selectedCreator?.liveTimeRange || '--'}
-
{selectedCreator.avgLiveGpm || '--'}
+
{selectedCreator?.avgLiveGpm || '--'}
LIVE GPM
-
{selectedCreator.liveVideos || '--'}
+
{selectedCreator?.liveVideos || '--'}
LIVE Videos
-
{selectedCreator.avgLiveViews || '--'}
+
{selectedCreator?.avgLiveViews || '--'}
Avg. LIVE Views
-
{selectedCreator.avgLiveEngagements || '--'}
+
{selectedCreator?.avgLiveEngagements || '--'}
Avg. LIVE Engagement
-
{selectedCreator.avgLiveLikes || '--'}
+
{selectedCreator?.avgLiveLikes || '--'}
Avg. LIVE Likes
- Shoppable LIVE{selectedCreator.liveTimeRange || '--'}
+ Shoppable LIVE{selectedCreator?.liveTimeRange || '--'}
-
{selectedCreator.avgLiveGpm || '--'}
+
{selectedCreator?.avgLiveGpm || '--'}
LIVE GPM
-
{selectedCreator.liveVideos || '--'}
+
{selectedCreator?.liveVideos || '--'}
LIVE Videos
-
{selectedCreator.avgLiveViews || '--'}
+
{selectedCreator?.avgLiveViews || '--'}
Avg. LIVE Views
-
{selectedCreator.avgLiveEngagements || '--'}
+
{selectedCreator?.avgLiveEngagements || '--'}
Avg. LIVE Engagement
-
{selectedCreator.avgLiveLikes || '--'}
+
{selectedCreator?.avgLiveLikes || '--'}
Avg. LIVE Likes
- Follwers{selectedCreator.liveTimeRange || '--'}
+ Follwers{selectedCreator?.liveTimeRange || '--'}
Top 5 Locations
@@ -399,7 +472,7 @@ function CreatorBasicInfo({ selectedCreator }) {
- Trends{selectedCreator.liveTimeRange || '--'}
+ Trends{selectedCreator?.liveTimeRange || '--'}
+
@@ -17,7 +17,7 @@ export default function Database({ path }) {
{path === 'youtube' &&
YouTube
}
-
-
+
+
);
}
diff --git a/src/pages/PrivateCreator.jsx b/src/pages/PrivateCreator.jsx
index f87d371..f20ba87 100644
--- a/src/pages/PrivateCreator.jsx
+++ b/src/pages/PrivateCreator.jsx
@@ -2,24 +2,23 @@ import React from 'react';
import SearchBar from '../components/SearchBar';
import { Button } from 'react-bootstrap';
import DatabaseFilter from '../components/DatabaseFilter';
-import CreatorList from '../components/CreatorList';
+import PrivateCreatorList from '../components/PrivateCreatorList';
export default function PrivateCreator({ path }) {
-
return (
-
-
-
-
-
-
-
Private Creators
- {path === 'tiktok' &&
TikTok
}
- {path === 'instagram' &&
Instagram
}
- {path === 'youtube' &&
YouTube
}
-
-
-
-
+
+
+
+
+
+
+
Private Creators
+ {path === 'tiktok' &&
TikTok
}
+ {path === 'instagram' &&
Instagram
}
+ {path === 'youtube' &&
YouTube
}
+
+
+
+
);
}
diff --git a/src/services/api.js b/src/services/api.js
new file mode 100644
index 0000000..50fac2c
--- /dev/null
+++ b/src/services/api.js
@@ -0,0 +1,61 @@
+import axios from 'axios';
+
+const api = axios.create({
+ baseURL: '/api',
+ withCredentials: true, // Include cookies if needed
+});
+
+api.interceptors.request.use(
+ (config) => {
+ const token = sessionStorage.getItem('token') || '03d9163150a8bfbbee2a33c4444237f337a35278';
+ if (token) {
+ config.headers.Authorization = `Token ${token}`;
+ }
+ return config;
+ },
+ (error) => {
+ console.error('Request error:', error);
+ return Promise.reject(error);
+ }
+);
+
+api.interceptors.response.use(
+ (response) => {
+ return response;
+ },
+ (error) => {
+ return Promise.reject(error);
+ }
+);
+
+const get = async (url, params) => {
+ const response = await api.get(url, params);
+ return response.data;
+};
+
+const post = async (url, data) => {
+ const response = await api.post(url, data);
+ return response.data;
+};
+
+const put = async (url, data) => {
+ const response = await api.put(url, data);
+ return response.data;
+};
+
+const del = async (url) => {
+ const response = await api.delete(url);
+ return response.data;
+};
+
+const upload = async (url, data) => {
+ const response = await api.post(url, data, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+ return response.data;
+};
+
+
+export default { get, post, put, del, upload };
diff --git a/src/store/slices/creatorsSlice.js b/src/store/slices/creatorsSlice.js
index 302be93..76c3ea4 100644
--- a/src/store/slices/creatorsSlice.js
+++ b/src/store/slices/creatorsSlice.js
@@ -1,4 +1,5 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
+import api from '../../services/api';
const mockVideos = [
{
id: 1,
@@ -32,12 +33,12 @@ export const mockCreators = [
name: 'name',
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=1',
category: 'Phones & Electronics',
- ecommerceLevel: 'L2',
- exposureLevel: 'KOC-1',
+ e_commerce_level: 'L2',
+ exposure_level: 'KOC-1',
followers: '162.2k',
gmv: '$534.1k',
soldPercentage: '18.1%',
- avgViews: '1.9k',
+ avg_video_views: '1.9k',
hasEcommerce: true,
hasTiktok: true,
verified: true,
@@ -49,12 +50,12 @@ export const mockCreators = [
name: 'name',
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=2',
category: 'Womenswear & Underwear',
- ecommerceLevel: 'L3',
- exposureLevel: 'KOL-3',
+ e_commerce_level: 'L3',
+ exposure_level: 'KOL-3',
followers: '162.2k',
gmv: '$534.1k',
soldPercentage: '18.1%',
- avgViews: '1.9k',
+ avg_video_views: '1.9k',
hasEcommerce: false,
hasTiktok: true,
verified: false,
@@ -66,12 +67,12 @@ export const mockCreators = [
name: 'name',
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=3',
category: 'Sports & Outdoor',
- ecommerceLevel: 'L4',
- exposureLevel: 'KOC-2',
+ e_commerce_level: 'L4',
+ exposure_level: 'KOC-2',
followers: '162.2k',
gmv: '$534.1k',
soldPercentage: '18.1%',
- avgViews: '1.9k',
+ avg_video_views: '1.9k',
hasEcommerce: true,
hasTiktok: true,
verified: false,
@@ -83,12 +84,12 @@ export const mockCreators = [
name: 'name',
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=4',
category: 'Food & Beverage',
- ecommerceLevel: 'L1',
- exposureLevel: 'KOC-2',
+ e_commerce_level: 'L1',
+ exposure_level: 'KOC-2',
followers: '162.2k',
gmv: '$534.1k',
soldPercentage: '18.1%',
- avgViews: '1.9k',
+ avg_video_views: '1.9k',
hasEcommerce: true,
hasTiktok: true,
hasInstagram: true,
@@ -102,12 +103,12 @@ export const mockCreators = [
name: 'name',
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=5',
category: 'Health',
- ecommerceLevel: 'L5',
- exposureLevel: 'KOL-2',
+ e_commerce_level: 'L5',
+ exposure_level: 'KOL-2',
followers: '162.2k',
gmv: '$534.1k',
soldPercentage: '18.1%',
- avgViews: '1.9k',
+ avg_video_views: '1.9k',
hasEcommerce: false,
hasTiktok: true,
hasInstagram: true,
@@ -121,12 +122,12 @@ export const mockCreators = [
name: 'name',
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=6',
category: 'Kitchenware',
- ecommerceLevel: 'New tag',
- exposureLevel: 'New tag',
+ e_commerce_level: 'New tag',
+ exposure_level: 'New tag',
followers: '162.2k',
gmv: '$534.1k',
soldPercentage: '18.1%',
- avgViews: '1.9k',
+ avg_video_views: '1.9k',
hasEcommerce: true,
hasTiktok: true,
hasInstagram: true,
@@ -138,66 +139,110 @@ export const mockCreators = [
];
// 模拟API获取数据的异步Thunk
-export const fetchCreators = createAsyncThunk('creators/fetchCreators', async ({ path }, { getState }) => {
- // 模拟API调用延迟
- await new Promise((resolve) => setTimeout(resolve, 500));
+// export const fetchCreators = createAsyncThunk('creators/fetchCreators', async ({ path }, { getState }) => {
+// // 模拟API调用延迟
+// await new Promise((resolve) => setTimeout(resolve, 500));
- // 获取当前的筛选条件
- const state = getState();
- const filters = state.filters;
+// // 获取当前的筛选条件
+// const state = getState();
+// const filters = state.filters;
- // 应用筛选逻辑(实际项目中可能在服务器端进行)
- let filteredCreators = [...mockCreators];
+// // 应用筛选逻辑(实际项目中可能在服务器端进行)
+// let filteredCreators = [...mockCreators];
- // 如果有选定的类别,进行筛选
- if (filters.category.length > 0) {
- filteredCreators = filteredCreators.filter((creator) => filters.category.includes(creator.category));
- }
+// // 如果有选定的类别,进行筛选
+// 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.ecommerceRatings.length > 0) {
+// filteredCreators = filteredCreators.filter((creator) =>
+// filters.ecommerceRatings.includes(creator.e_commerce_level)
+// );
+// }
- // 如果有选定的曝光评级,进行筛选
- if (filters.exposureRatings.length > 0) {
- filteredCreators = filteredCreators.filter((creator) =>
- filters.exposureRatings.includes(creator.exposureLevel)
- );
- }
+// // 如果有选定的曝光评级,进行筛选
+// if (filters.exposureRatings.length > 0) {
+// filteredCreators = filteredCreators.filter((creator) =>
+// filters.exposureRatings.includes(creator.exposure_level)
+// );
+// }
- // 筛选观看量范围
- if (filters.viewsRange.length === 2) {
- const minViews = filters.viewsRange[0];
- const maxViews = filters.viewsRange[1];
+// // 筛选观看量范围
+// 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;
- }
+// filteredCreators = filteredCreators.filter((creator) => {
+// // 将带k的字符串转换为数字
+// const viewsStr = creator.avg_video_views;
+// let views = parseFloat(viewsStr);
+// if (viewsStr.includes('k')) {
+// views *= 1000;
+// } else if (viewsStr.includes('M')) {
+// views *= 1000000;
+// }
- return views >= minViews && views <= maxViews;
- });
- }
+// return views >= minViews && views <= maxViews;
+// });
+// }
- return filteredCreators;
-});
+// return filteredCreators;
+// });
const initialState = {
- creators: [],
+ publicCreators: [],
+ privateCreators: [],
+ publicTiktokCreators: [],
+ publicInstagramCreators: [],
+ publicYoutubeCreators: [],
+ privateTiktokCreators: [],
+ privateInstagramCreators: [],
+ privateYoutubeCreators: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
selectedCreators: [],
selectedCreator: null,
+ pagination: {
+ current_page: 1,
+ total_pages: 0,
+ total_count: 0,
+ has_next: false,
+ has_prev: false,
+ },
+ hasMore: true,
+ isLoadingMore: false,
};
+export const fetchCreators = createAsyncThunk('creators/fetchCreators', async ({ path, page = 1 }, { getState }) => {
+ const state = getState();
+ const filters = state.filters;
+
+ const response = await api.get(`/daren_detail/public/creators`, { params: { page } });
+ return response;
+});
+
+export const fetchPrivateCreators = createAsyncThunk(
+ 'creators/fetchPrivateCreators',
+ async ({ path, page = 1 }, { getState }) => {
+ const state = getState();
+ const filters = state.filters;
+
+ const queryParams = { pool_id: 1, page };
+ const response = await api.get(`/daren_detail/private/pools/creators`, { params: queryParams });
+ return response;
+ }
+);
+
+export const fetchCreatorDetail = createAsyncThunk(
+ 'creators/fetchCreatorDetail',
+ async ({ creatorId }, { getState }) => {
+ const response = await api.get(`/daren_detail/creators/${creatorId}`);
+ return response;
+ }
+);
+
const creatorsSlice = createSlice({
name: 'creators',
initialState,
@@ -218,32 +263,92 @@ const creatorsSlice = createSlice({
clearCreatorSelection: (state) => {
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;
},
+ resetCreators: (state) => {
+ state.publicCreators = [];
+ state.privateCreators = [];
+ state.pagination = initialState.pagination;
+ state.hasMore = true;
+ state.isLoadingMore = false;
+ },
},
extraReducers: (builder) => {
builder
.addCase(fetchCreators.pending, (state) => {
- state.status = 'loading';
+ if (state.creators.length === 0) {
+ state.status = 'loading';
+ } else {
+ state.isLoadingMore = true;
+ }
})
.addCase(fetchCreators.fulfilled, (state, action) => {
state.status = 'succeeded';
- state.creators = action.payload;
+ state.isLoadingMore = false;
+ const { data, pagination } = action.payload;
+
+ if (pagination.current_page === 1) {
+ state.publicCreators = data;
+ } else {
+ state.publicCreators = [...state.publicCreators, ...data];
+ }
+ state.pagination = pagination;
+ state.hasMore = pagination.has_next;
})
.addCase(fetchCreators.rejected, (state, action) => {
- console.log(action);
+ state.status = 'failed';
+ state.isLoadingMore = false;
+ state.error = action.error.message;
+ })
+ .addCase(fetchPrivateCreators.pending, (state) => {
+ if (state.creators?.length === 0) {
+ state.status = 'loading';
+ } else {
+ state.isLoadingMore = true;
+ }
+ })
+ .addCase(fetchPrivateCreators.fulfilled, (state, action) => {
+ state.status = 'succeeded';
+ state.isLoadingMore = false;
+ const { data, pagination } = action.payload;
+
+ if (pagination.current_page === 1) {
+ state.privateCreators = data;
+ } else {
+ state.privateCreators = [...state.privateCreators, ...data];
+ }
+ state.pagination = pagination;
+ state.hasMore = pagination.has_next;
+ })
+ .addCase(fetchPrivateCreators.rejected, (state, action) => {
+ console.log('fetchPrivateCreators.rejected', action);
+ state.status = 'failed';
+ state.isLoadingMore = false;
+ state.error = action.error.message;
+ })
+ .addCase(fetchCreatorDetail.pending, (state) => {
+ state.status = 'loading';
+ })
+ .addCase(fetchCreatorDetail.fulfilled, (state, action) => {
+ state.status = 'succeeded';
+ const { data, pagination } = action.payload;
+ state.selectedCreator = data;
+ })
+ .addCase(fetchCreatorDetail.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
},
});
-export const { toggleCreatorSelection, selectAllCreators, clearCreatorSelection, selectCreator, clearCreator } =
- creatorsSlice.actions;
+export const {
+ toggleCreatorSelection,
+ selectAllCreators,
+ clearCreatorSelection,
+ clearCreator,
+ setCreators,
+ resetCreators,
+} = creatorsSlice.actions;
export default creatorsSlice.reducer;
diff --git a/src/styles/CreatorDiscovery.scss b/src/styles/CreatorDiscovery.scss
index 802f161..504d786 100644
--- a/src/styles/CreatorDiscovery.scss
+++ b/src/styles/CreatorDiscovery.scss
@@ -53,6 +53,9 @@
}
.creator-detail-page {
+ display: flex;
+ flex-flow: column nowrap;
+ gap: 1rem;
.creator-info-detail-container {
display: flex;
flex-direction: row;
@@ -78,6 +81,7 @@
height: 115px;
border-radius: 50%;
overflow: hidden;
+ background-color: $secondary-200;
}
.creator-info-right {
display: flex;
@@ -108,6 +112,28 @@
.creator-info-value {
}
}
+ .category-info {
+ .creator-info-value {
+ display: flex;
+ flex-flow: row nowrap;
+ gap: 0.5rem;
+ }
+ }
+ .collab-info {
+ .creator-info-value {
+ display: flex;
+ flex-flow: row nowrap;
+ gap: 0.5rem;
+ }
+ }
+ .badge {
+ padding: 6px 8px;
+ border-radius: 12px;
+ color: $neutral-600;
+ background-color: $neutral-200;
+ font-size: 0.875rem;
+ font-weight: 500;
+ }
}
.creator-info-3 {
display: flex;
@@ -117,6 +143,7 @@
display: flex;
flex-flow: row nowrap;
color: $neutral-900;
+ align-items: center;
gap: 0.5rem;
.creator-info-label {
@@ -202,10 +229,10 @@
color: $neutral-900;
margin-bottom: 1rem;
}
- // canvas {
- // max-width: 260px;
- // max-height: 260px;
- // }
+ canvas {
+ max-width: 280px;
+ max-height: 200px;
+ }
}
}
}
@@ -267,6 +294,8 @@
flex-flow: column nowrap;
justify-content: space-between;
canvas {
+ max-width: 280px;
+ max-height: 200px;
}
}
}
@@ -290,7 +319,7 @@
display: flex;
flex-flow: column nowrap;
gap: 0.5rem;
- font-size: .875rem;
+ font-size: 0.875rem;
.video-title {
font-weight: 700;
text-overflow: ellipsis;
@@ -312,7 +341,6 @@
align-items: center;
}
}
-
}
}
}
diff --git a/src/styles/DatabaseList.scss b/src/styles/DatabaseList.scss
index fcb1d00..b30be13 100644
--- a/src/styles/DatabaseList.scss
+++ b/src/styles/DatabaseList.scss
@@ -3,17 +3,28 @@
// 导入变量
@import './variables';
+.database-page {
+ height: 100%;
+ position: relative;
+}
+.private-creator-page {
+ height: 100%;
+ position: relative;
+}
.creator-database-table {
+ height: 100%;
+ position: relative;
+
.creator-cell {
.creator-avatar {
position: relative;
- width: 36px;
- height: 36px;
margin-right: 12px;
+ flex-shrink: 0;
img {
- width: 100%;
- height: 100%;
+ display: block;
+ width: 36px;
+ height: 36px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #e2e8f0;
@@ -37,7 +48,8 @@
.creator-name {
font-weight: 700;
- color: #090909
+ color: #090909;
+ white-space: nowrap;
}
}
@@ -132,4 +144,22 @@
color: #000000;
}
}
+
+ .table-container {
+ position: relative;
+ max-height: calc(100% - 455px); // Adjust this value based on your layout
+ overflow-y: auto;
+
+ .sticky-header {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ background: white;
+
+ th {
+ background: white;
+ border-bottom: 2px solid #dee2e6;
+ }
+ }
+ }
}
diff --git a/src/styles/global.scss b/src/styles/global.scss
index 52d8502..598c310 100644
--- a/src/styles/global.scss
+++ b/src/styles/global.scss
@@ -31,4 +31,11 @@
.btn {
display: inline-flex !important;
align-items: center;
+}
+.back-button {
+ width: max-content;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ cursor: pointer;
}
\ No newline at end of file
diff --git a/src/styles/sidebar.scss b/src/styles/sidebar.scss
index 2062263..7283c26 100644
--- a/src/styles/sidebar.scss
+++ b/src/styles/sidebar.scss
@@ -151,6 +151,7 @@
transition: all 0.3s ease;
background: #f8f9fa;
border-radius: 8px;
+ overflow: hidden;
}
// Collapsed sidebar adjustments
diff --git a/vite.config.js b/vite.config.js
index 531f180..484893d 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,26 +1,37 @@
-import { defineConfig } from 'vite';
+import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
// https://vite.dev/config/
-export default defineConfig({
- plugins: [react()],
- resolve: {
- alias: {
- '@': path.resolve(__dirname, './src'),
- },
- },
- css: {
- preprocessorOptions: {
- scss: {
- quietDeps: true,
- outputStyle: 'compressed',
+export default defineConfig(({ mode }) => {
+ const env = loadEnv(mode, process.cwd());
+ return {
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
},
},
- devSourcemap: false,
- },
- build: {
- cssCodeSplit: false,
- minify: true,
- },
+ css: {
+ preprocessorOptions: {
+ scss: {
+ quietDeps: true,
+ outputStyle: 'compressed',
+ },
+ },
+ devSourcemap: false,
+ },
+ build: {
+ cssCodeSplit: false,
+ minify: true,
+ },
+ server: {
+ proxy: {
+ '/api': {
+ target: env.VITE_API_URL || 'http://81.69.223.133:8008',
+ changeOrigin: true,
+ },
+ },
+ },
+ };
});