mirror of
https://github.com/Funkoala14/CreatorCenter_OOIN.git
synced 2025-06-07 22:58:14 +08:00
Compare commits
2 Commits
9706970690
...
2be18a979d
Author | SHA1 | Date | |
---|---|---|---|
2be18a979d | |||
166c5f5271 |
2
.env
Normal file
2
.env
Normal file
@ -0,0 +1,2 @@
|
||||
VITE_PROD = false
|
||||
VITE_API_URL = "http://81.69.223.133:58099"
|
2
.env.development
Normal file
2
.env.development
Normal file
@ -0,0 +1,2 @@
|
||||
VITE_PROD = false
|
||||
VITE_API_URL = "http://81.69.223.133:58099"
|
2
.env.production
Normal file
2
.env.production
Normal file
@ -0,0 +1,2 @@
|
||||
VITE_PROD = false
|
||||
VITE_API_URL = "http://81.69.223.133:58099"
|
@ -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
|
||||
|
164
package-lock.json
generated
164
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
BIN
src/assets/placeholder.png
Normal file
BIN
src/assets/placeholder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@ -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 (
|
||||
|
@ -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 (
|
||||
<div className='text-center p-5'>
|
||||
<Spinner animation='border' role='status' variant='primary'>
|
||||
@ -87,13 +110,14 @@ export default function CreatorList({ path, pageType = 'database' }) {
|
||||
|
||||
return (
|
||||
<div className='creator-database-table'>
|
||||
<div className='table-container'>
|
||||
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden'>
|
||||
<thead>
|
||||
<thead className='sticky-header'>
|
||||
<tr>
|
||||
<th className='selector' style={{ width: '40px' }}>
|
||||
<Form.Check
|
||||
type='checkbox'
|
||||
checked={selectedCreators.length === creators.length && creators.length > 0}
|
||||
checked={selectedCreators.length === publicCreators.length && publicCreators.length > 0}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
</th>
|
||||
@ -107,11 +131,11 @@ export default function CreatorList({ path, pageType = 'database' }) {
|
||||
>
|
||||
Category {renderSortIcon('category')}
|
||||
</th>
|
||||
<th className='e-commerce-level text-center' onClick={() => handleSort('ecommerceLevel')}>
|
||||
E-commerce Level {renderSortIcon('ecommerceLevel')}
|
||||
<th className='e-commerce-level text-center' onClick={() => handleSort('e_commerce_level')}>
|
||||
E-commerce Level {renderSortIcon('e_commerce_level')}
|
||||
</th>
|
||||
<th className='exposure-level text-center' onClick={() => handleSort('exposureLevel')}>
|
||||
Exposure Level {renderSortIcon('exposureLevel')}
|
||||
<th className='exposure-level text-center' onClick={() => handleSort('exposure_level')}>
|
||||
Exposure Level {renderSortIcon('exposure_level')}
|
||||
</th>
|
||||
<th className='followers text-center' onClick={() => handleSort('followers')}>
|
||||
Followers {renderSortIcon('followers')}
|
||||
@ -119,44 +143,40 @@ export default function CreatorList({ path, pageType = 'database' }) {
|
||||
<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 className='views text-center' onClick={() => handleSort('avg_video_views')}>
|
||||
Avg. Video Views {renderSortIcon('avg_video_views')}
|
||||
</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='profile text-center'>Profile</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{creators.length === 0 ? (
|
||||
{!publicCreators || publicCreators.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' : ''}>
|
||||
publicCreators.map((creator) => (
|
||||
<tr
|
||||
key={creator.creator_id}
|
||||
className={selectedCreators.includes(creator.creator_id) ? 'selected' : ''}
|
||||
>
|
||||
<td>
|
||||
<Form.Check
|
||||
type='checkbox'
|
||||
checked={selectedCreators.includes(creator.id)}
|
||||
onChange={() => handleSelectCreator(creator.id)}
|
||||
checked={selectedCreators.includes(creator.creator_id)}
|
||||
onChange={() => handleSelectCreator(creator.creator_id)}
|
||||
/>
|
||||
</td>
|
||||
<td className='creator-cell'>
|
||||
<div className='d-flex align-items-center'>
|
||||
<div className='creator-avatar'>
|
||||
<img src={creator.avatar} alt={creator.name} />
|
||||
{creator.verified && <span className='verified-badge'></span>}
|
||||
{creator.status && <span className='verified-badge'></span>}
|
||||
</div>
|
||||
<Link to={`/creator/${creator.id}`} className='creator-name'>
|
||||
<Link to={`/creator/${creator.creator_id}`} className='creator-name'>
|
||||
{creator.name}
|
||||
</Link>
|
||||
</div>
|
||||
@ -167,11 +187,14 @@ export default function CreatorList({ path, pageType = 'database' }) {
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center'>
|
||||
<span className='level-badge ecommerce-level'>{creator.ecommerceLevel}</span>
|
||||
<span className='level-badge ecommerce-level'>{creator.e_commerce_level}</span>
|
||||
</td>
|
||||
<td className='text-center'>
|
||||
<span className='level-badge exposure-level' data-level={creator.exposureLevel}>
|
||||
{creator.exposureLevel}
|
||||
<span
|
||||
className='level-badge exposure-level'
|
||||
data-level={creator.exposure_level}
|
||||
>
|
||||
{creator.exposure_level}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-nowrap text-center'>{creator.followers}</td>
|
||||
@ -179,14 +202,7 @@ export default function CreatorList({ path, pageType = 'database' }) {
|
||||
<div>{creator.gmv}</div>
|
||||
<div className='small text-muted'>Items Sold: {creator.soldPercentage}</div>
|
||||
</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-nowrap text-center'>{creator.avg_video_views}</td>
|
||||
<td className='text-center'>
|
||||
{creator.hasEcommerce ? <div className='colored-dot blue mx-auto'></div> : null}
|
||||
</td>
|
||||
@ -202,6 +218,14 @@ export default function CreatorList({ path, pageType = 'database' }) {
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
{hasMore && (
|
||||
<div ref={loadingRef} className='text-center p-3'>
|
||||
<Spinner animation='border' role='status' variant='primary' size='sm'>
|
||||
<span className='visually-hidden'>Loading more...</span>
|
||||
</Spinner>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -78,8 +78,8 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
|
||||
// 组件加载时获取数据
|
||||
useEffect(() => {
|
||||
dispatch(fetchCreators({ path }));
|
||||
}, [dispatch, filters]);
|
||||
|
||||
}, [dispatch, filters, pageType, path]);
|
||||
|
||||
// 处理类别选择
|
||||
const handleCategorySelect = (category) => {
|
||||
|
230
src/components/PrivateCreatorList.jsx
Normal file
230
src/components/PrivateCreatorList.jsx
Normal file
@ -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' ? <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' && (!privateCreators || privateCreators.length === 0)) {
|
||||
return (
|
||||
<div className='text-center p-5'>
|
||||
<Spinner animation='border' role='status' variant='primary'>
|
||||
<span className='visually-hidden'>Loading...</span>
|
||||
</Spinner>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果加载失败,显示错误信息
|
||||
if (status === 'failed') {
|
||||
return <div className='alert alert-danger'>Failed to load creators. Please try again later.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='creator-database-table'>
|
||||
<div className='table-container'>
|
||||
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden'>
|
||||
<thead className='sticky-header'>
|
||||
<tr>
|
||||
<th className='selector' style={{ width: '40px' }}>
|
||||
<Form.Check
|
||||
type='checkbox'
|
||||
checked={selectedCreators.length === privateCreators.length && privateCreators.length > 0}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
</th>
|
||||
<th className='creator' onClick={() => handleSort('name')} style={{ width: '180px' }}>
|
||||
Creator {renderSortIcon('name')}
|
||||
</th>
|
||||
<th
|
||||
className='category text-center'
|
||||
onClick={() => handleSort('category')}
|
||||
style={{ width: '180px' }}
|
||||
>
|
||||
Category {renderSortIcon('category')}
|
||||
</th>
|
||||
<th className='e-commerce-level text-center' onClick={() => handleSort('e_commerce_level')}>
|
||||
E-commerce Level {renderSortIcon('e_commerce_level')}
|
||||
</th>
|
||||
<th className='exposure-level text-center' onClick={() => handleSort('exposure_level')}>
|
||||
Exposure Level {renderSortIcon('exposure_level')}
|
||||
</th>
|
||||
<th className='followers text-center' onClick={() => handleSort('followers')}>
|
||||
Followers {renderSortIcon('followers')}
|
||||
</th>
|
||||
<th className='gmv text-center' onClick={() => handleSort('gmv')}>
|
||||
GMV {renderSortIcon('gmv')}
|
||||
</th>
|
||||
<th className='views text-center' onClick={() => handleSort('avg_video_views')}>
|
||||
Avg. Video Views {renderSortIcon('avg_video_views')}
|
||||
</th>
|
||||
<th className='pricing text-center'>Pricing</th>
|
||||
<th className='collab-count text-center'># Collab</th>
|
||||
<th className='latest-collab text-center'>Latest Collab.</th>
|
||||
<th className='e-commerce text-center'>E-commerce</th>
|
||||
<th className='profile text-center'>Profile</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!privateCreators || privateCreators.length <= 0 ? (
|
||||
<tr>
|
||||
<td colSpan='13' className='text-center py-4'>
|
||||
No creators found matching your filters.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
privateCreators.map((creator) => (
|
||||
<tr
|
||||
key={creator.creator_id}
|
||||
className={selectedCreators.includes(creator.creator_id) ? 'selected' : ''}
|
||||
>
|
||||
<td>
|
||||
<Form.Check
|
||||
type='checkbox'
|
||||
checked={selectedCreators.includes(creator.creator_id)}
|
||||
onChange={() => handleSelectCreator(creator.creator_id)}
|
||||
/>
|
||||
</td>
|
||||
<td className='creator-cell'>
|
||||
<div className='d-flex align-items-center'>
|
||||
<div className='creator-avatar'>
|
||||
<img src={creator.avatar} alt={creator.name} />
|
||||
{creator.status && <span className='verified-badge'></span>}
|
||||
</div>
|
||||
<Link to={`/creator/${creator.creator_id}`} className='creator-name'>
|
||||
{creator.name}
|
||||
</Link>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span className={`category-pill ${getCategoryClassName(creator.category)}`}>
|
||||
{creator.category}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center'>
|
||||
<span className='level-badge ecommerce-level'>{creator.e_commerce_level}</span>
|
||||
</td>
|
||||
<td className='text-center'>
|
||||
<span
|
||||
className='level-badge exposure-level'
|
||||
data-level={creator.exposure_level}
|
||||
>
|
||||
{creator.exposure_level}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-nowrap text-center'>{creator.followers}</td>
|
||||
<td className='text-center'>
|
||||
<div>{creator.gmv}</div>
|
||||
<div className='small text-muted'>Items Sold: {creator.soldPercentage}</div>
|
||||
</td>
|
||||
<td className='text-nowrap text-center'>{creator.avg_video_views}</td>
|
||||
<td className='text-center'>{creator.pricing}</td>
|
||||
<td className='text-center'>{creator.collab_count}</td>
|
||||
<td className='text-center'>{creator.latestCollab}</td>
|
||||
<td className='text-center'>
|
||||
{creator.hasEcommerce ? <div className='colored-dot blue mx-auto'></div> : null}
|
||||
</td>
|
||||
<td className='text-center'>
|
||||
{creator.hasTiktok && (
|
||||
<div className='social-icon tiktok-icon mx-auto'>
|
||||
<FontAwesomeIcon icon='fa-brands fa-tiktok' />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
{hasMore && (
|
||||
<div ref={loadingRef} className='text-center p-3'>
|
||||
<Spinner animation='border' role='status' variant='primary' size='sm'>
|
||||
<span className='visually-hidden'>Loading more...</span>
|
||||
</Spinner>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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(
|
||||
<StrictMode>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</StrictMode>
|
||||
);
|
||||
|
@ -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 <div>Loading...</div>;
|
||||
}
|
||||
if (status === 'failed') {
|
||||
return <div>Failed to load creator detail</div>;
|
||||
}
|
||||
|
||||
if (!selectedCreator) {
|
||||
return <div>No creator found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='creator-detail-page'>
|
||||
<div className='back-button' onClick={handleBack}>
|
||||
@ -80,35 +128,50 @@ export default function CreatorDetail({}) {
|
||||
<div className='creator-info-container card'>
|
||||
<div className='creator-info-1'>
|
||||
<div className='creator-avatar'>
|
||||
<img src={selectedCreator.avatar} alt={selectedCreator.name} />
|
||||
<img src={selectedCreator.creator.avatar} alt={selectedCreator.creator.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-name'>{selectedCreator.creator.name}</div>
|
||||
<div className='creator-desc'>{selectedCreator?.description || '--'}</div>
|
||||
<div className='creator-category'>{selectedCreator.business?.category || '--'}</div>
|
||||
<div className='creator-location'>
|
||||
<MapPin size={16} />
|
||||
{selectedCreator.location || ' --'}
|
||||
{selectedCreator.creator.location || ' --'}
|
||||
</div>
|
||||
<div className='creator-live-time'>{selectedCreator.liveTime || '--'}</div>
|
||||
<div className='creator-live-time'>{selectedCreator.live_schedule || '--'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='creator-info-2'>
|
||||
<div className='creator-info-item'>
|
||||
<div className='creator-info-item category-info'>
|
||||
<div className='creator-info-label'>Category</div>
|
||||
<div className='creator-info-value'>{selectedCreator.category}</div>
|
||||
<div className='creator-info-value'>
|
||||
{selectedCreator.business?.categories.map((item, index) => (
|
||||
<span className='category-item badge' key={index}>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='creator-info-item'>
|
||||
<div className='creator-info-label'>MCN</div>
|
||||
<div className='creator-info-value'>{selectedCreator.mcn || '--'}</div>
|
||||
<div className='creator-info-value'>{selectedCreator.business?.mcn || '--'}</div>
|
||||
</div>
|
||||
<div className='creator-info-item'>
|
||||
<div className='creator-info-label'>Pricing</div>
|
||||
<div className='creator-info-value'>{selectedCreator.pricing}</div>
|
||||
<div className='creator-info-value'>
|
||||
{selectedCreator.business?.pricing?.range || '--'}
|
||||
</div>
|
||||
<div className='creator-info-item'>
|
||||
</div>
|
||||
<div className='creator-info-item collab-info'>
|
||||
<div className='creator-info-label'>Collab.</div>
|
||||
<div className='creator-info-value'>{selectedCreator.collab || '--'}</div>
|
||||
<div className='creator-info-value'>
|
||||
<span className='collab-count'>
|
||||
{selectedCreator.business?.collab_count || '--'}
|
||||
</span>
|
||||
<span className='latest-collab badge'>
|
||||
{selectedCreator.business?.latest_collab || '--'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='creator-info-3'>
|
||||
@ -116,19 +179,23 @@ export default function CreatorDetail({}) {
|
||||
<div className='creator-info-label mail'>
|
||||
<Mail size={18} />
|
||||
</div>
|
||||
<div className='creator-info-value'>{selectedCreator.email || '--'}</div>
|
||||
<div className='creator-info-value'>{selectedCreator.creator.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 className='creator-info-value'>
|
||||
{selectedCreator.creator.social_accounts?.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 className='creator-info-value'>
|
||||
{selectedCreator.creator.social_accounts?.tiktok || '--'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -136,47 +203,53 @@ export default function CreatorDetail({}) {
|
||||
<div className='levels'>
|
||||
<div className='level-item'>
|
||||
<div className='name'>E-commerce Level</div>
|
||||
<div className='value'>{selectedCreator.ecommerceLevel || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.metrics?.e_commerce_level || '--'}</div>
|
||||
</div>
|
||||
<div className='level-item'>
|
||||
<div className='name'>Exposure Level</div>
|
||||
<div className='value'>{selectedCreator.exposureLevel || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.metrics?.exposure_level || '--'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='data-cards'>
|
||||
<div className='data-card'>
|
||||
<div className='value'>{selectedCreator.followers || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.metrics?.followers || '--'}</div>
|
||||
<div className='name'>Followers</div>
|
||||
</div>
|
||||
<div className='data-card'>
|
||||
<div className='value'>{selectedCreator.gmv || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.metrics?.gmv || '--'}</div>
|
||||
<div className='name'>GMV</div>
|
||||
</div>
|
||||
<div className='data-card'>
|
||||
<div className='value'>{selectedCreator.avgVideoViews || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.metrics?.avg_video_views || '--'}</div>
|
||||
<div className='name'>Avg Video Views</div>
|
||||
</div>
|
||||
<div className='data-card'>
|
||||
<div className='value'>{selectedCreator.itemsSold || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.metrics?.items_sold || '--'}</div>
|
||||
<div className='name'>Items Sold</div>
|
||||
</div>
|
||||
<div className='data-card'>
|
||||
<div className='value'>{selectedCreator.gpm || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.metrics?.gpm || '--'}</div>
|
||||
<div className='name'>GPM</div>
|
||||
</div>
|
||||
<div className='data-card'>
|
||||
<div className='value'>{selectedCreator.gpmPerCustomer || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.metrics?.gmv_per_customer || '--'}</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, cutout: 80 }} />
|
||||
<Doughnut
|
||||
data={processChartData(selectedCreator?.analytics?.gmv_by_channel, 'channel')}
|
||||
options={{ ...options, cutout: 50 }}
|
||||
/>
|
||||
</div>
|
||||
<div className='data-chart'>
|
||||
<div className='chart-title'>GMV by product category</div>
|
||||
<Doughnut data={data} options={{ ...options, cutout: 80 }} />
|
||||
<Doughnut
|
||||
data={processChartData(selectedCreator?.analytics?.gmv_by_category, 'category')}
|
||||
options={{ ...options, cutout: 50 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -262,134 +335,134 @@ function CreatorBasicInfo({ selectedCreator }) {
|
||||
<div className='basic-info-list'>
|
||||
<div className='basic-info-title'>Collaboration Metrics</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgCommissionRate || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avg_commission_rate || '--'}</div>
|
||||
<div className='name'>Avg. Commission Rate</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.products || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.products || '--'}</div>
|
||||
<div className='name'>Products</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.brandCollaborations || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.brand_collaborations || '--'}</div>
|
||||
<div className='name'>Brand Collaborations</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.productPrice || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.product_price || '--'}</div>
|
||||
<div className='name'>Product Price</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='basic-info-list'>
|
||||
<div className='basic-info-title'>
|
||||
Video<span className='time-range'>{selectedCreator.videoTimeRange || '--'}</span>
|
||||
Video<span className='time-range'>{selectedCreator?.videoTimeRange || '--'}</span>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgVideoGpm || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgVideoGpm || '--'}</div>
|
||||
<div className='name'>Video GPM</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.videos.length || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.videos?.length || '--'}</div>
|
||||
<div className='name'>Videos</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgVideoViews || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgVideoViews || '--'}</div>
|
||||
<div className='name'>Avg. Video Views</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgVideoEngagements || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgVideoEngagements || '--'}</div>
|
||||
<div className='name'>Avg. Video Engagement</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgVideoLikes || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgVideoLikes || '--'}</div>
|
||||
<div className='name'>Avg. Video Likes</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='basic-info-list'>
|
||||
<div className='basic-info-title'>
|
||||
Shoppable Video<span className='time-range'>{selectedCreator.videoTimeRange || '--'}</span>
|
||||
Shoppable Video<span className='time-range'>{selectedCreator?.videoTimeRange || '--'}</span>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgVideoGpm || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgVideoGpm || '--'}</div>
|
||||
<div className='name'>Video GPM</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.videos.length || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.videos?.length || '--'}</div>
|
||||
<div className='name'>Videos</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgVideoViews || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgVideoViews || '--'}</div>
|
||||
<div className='name'>Avg. Video Views</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgVideoEngagements || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgVideoEngagements || '--'}</div>
|
||||
<div className='name'>Avg. Video Engagement</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgVideoLikes || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgVideoLikes || '--'}</div>
|
||||
<div className='name'>Avg. Video Likes</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='basic-info-list'>
|
||||
<div className='basic-info-title'>
|
||||
LIVE<span className='time-range'>{selectedCreator.liveTimeRange || '--'}</span>
|
||||
LIVE<span className='time-range'>{selectedCreator?.liveTimeRange || '--'}</span>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgLiveGpm || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgLiveGpm || '--'}</div>
|
||||
<div className='name'>LIVE GPM</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.liveVideos || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.liveVideos || '--'}</div>
|
||||
<div className='name'>LIVE Videos</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgLiveViews || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgLiveViews || '--'}</div>
|
||||
<div className='name'>Avg. LIVE Views</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgLiveEngagements || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgLiveEngagements || '--'}</div>
|
||||
<div className='name'>Avg. LIVE Engagement</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgLiveLikes || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgLiveLikes || '--'}</div>
|
||||
<div className='name'>Avg. LIVE Likes</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='basic-info-list'>
|
||||
<div className='basic-info-title'>
|
||||
Shoppable LIVE<span className='time-range'>{selectedCreator.liveTimeRange || '--'}</span>
|
||||
Shoppable LIVE<span className='time-range'>{selectedCreator?.liveTimeRange || '--'}</span>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgLiveGpm || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgLiveGpm || '--'}</div>
|
||||
<div className='name'>LIVE GPM</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.liveVideos || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.liveVideos || '--'}</div>
|
||||
<div className='name'>LIVE Videos</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgLiveViews || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgLiveViews || '--'}</div>
|
||||
<div className='name'>Avg. LIVE Views</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgLiveEngagements || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgLiveEngagements || '--'}</div>
|
||||
<div className='name'>Avg. LIVE Engagement</div>
|
||||
</div>
|
||||
<div className='basic-info-item'>
|
||||
<div className='value'>{selectedCreator.avgLiveLikes || '--'}</div>
|
||||
<div className='value'>{selectedCreator?.avgLiveLikes || '--'}</div>
|
||||
<div className='name'>Avg. LIVE Likes</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='basic-info-list'>
|
||||
<div className='basic-info-title'>
|
||||
Follwers<span className='time-range'>{selectedCreator.liveTimeRange || '--'}</span>
|
||||
Follwers<span className='time-range'>{selectedCreator?.liveTimeRange || '--'}</span>
|
||||
</div>
|
||||
<div className='followers-data-charts'>
|
||||
<div className='data-chart'>
|
||||
<div className='chart-title'>Follower Gender</div>
|
||||
<Doughnut data={data} options={{ ...options, cutout: 90 }} />
|
||||
<Doughnut data={data} options={{ ...options, cutout: 60 }} />
|
||||
</div>
|
||||
<div className='data-chart'>
|
||||
<div className='chart-title'>Follower Age</div>
|
||||
<Doughnut data={data} options={{ ...options, cutout: 90 }} />
|
||||
<Doughnut data={data} options={{ ...options, cutout: 60 }} />
|
||||
</div>
|
||||
<div className='data-chart'>
|
||||
<div className='chart-title'>Top 5 Locations</div>
|
||||
@ -399,7 +472,7 @@ function CreatorBasicInfo({ selectedCreator }) {
|
||||
</div>
|
||||
<div className='basic-info-list'>
|
||||
<div className='basic-info-title'>
|
||||
Trends<span className='time-range'>{selectedCreator.liveTimeRange || '--'}</span>
|
||||
Trends<span className='time-range'>{selectedCreator?.liveTimeRange || '--'}</span>
|
||||
</div>
|
||||
<div className='tab-switches'>
|
||||
<div
|
||||
|
@ -5,7 +5,7 @@ import SearchBar from '../components/SearchBar';
|
||||
import { Button } from 'react-bootstrap';
|
||||
export default function Database({ path }) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className='database-page'>
|
||||
<div className='function-bar'>
|
||||
<SearchBar />
|
||||
<Button>+ Add to Campaign</Button>
|
||||
@ -17,7 +17,7 @@ export default function Database({ path }) {
|
||||
{path === 'youtube' && <div className='breadcrumb-item'>YouTube</div>}
|
||||
</div>
|
||||
<DatabaseFilter path={path} pageType={'database'} />
|
||||
<CreatorList path={path} pageType={'database'} />
|
||||
</React.Fragment>
|
||||
<CreatorList path={path} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -2,12 +2,11 @@ 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 (
|
||||
<React.Fragment>
|
||||
<div className='private-creator-page'>
|
||||
<div className='function-bar'>
|
||||
<SearchBar />
|
||||
<Button>+ Add to Campaign</Button>
|
||||
@ -19,7 +18,7 @@ export default function PrivateCreator({ path }) {
|
||||
{path === 'youtube' && <div className='breadcrumb-item'>YouTube</div>}
|
||||
</div>
|
||||
<DatabaseFilter path={path} pageType={'private'} />
|
||||
<CreatorList path={path} pageType={'private'} />
|
||||
</React.Fragment>
|
||||
<PrivateCreatorList path={path} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
61
src/services/api.js
Normal file
61
src/services/api.js
Normal file
@ -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 };
|
@ -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) => {
|
||||
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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,3 +32,10 @@
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
}
|
||||
.back-button {
|
||||
width: max-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
@ -151,6 +151,7 @@
|
||||
transition: all 0.3s ease;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Collapsed sidebar adjustments
|
||||
|
@ -1,9 +1,11 @@
|
||||
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({
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd());
|
||||
return {
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
@ -23,4 +25,13 @@ export default defineConfig({
|
||||
cssCodeSplit: false,
|
||||
minify: true,
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: env.VITE_API_URL || 'http://81.69.223.133:8008',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user