mirror of
https://github.com/Funkoala14/CreatorCenter_OOIN.git
synced 2025-06-08 05:28: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,
|
name: string,
|
||||||
avatar: string,
|
avatar: string,
|
||||||
category: string,
|
category: string,
|
||||||
ecommerceLevel: string,
|
e_commerce_level: string,
|
||||||
exposureLevel: string,
|
exposure_level: string,
|
||||||
followers: string,
|
followers: string,
|
||||||
gmv: string,
|
gmv: string,
|
||||||
soldPercentage: string,
|
soldPercentage: string,
|
||||||
avgViews: string,
|
avg_video_views: string,
|
||||||
hasEcommerce: boolean,
|
hasEcommerce: boolean,
|
||||||
hasTiktok: boolean,
|
hasTiktok: boolean,
|
||||||
hasInstagram: boolean, // optional
|
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/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
"@reduxjs/toolkit": "^2.8.1",
|
"@reduxjs/toolkit": "^2.8.1",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"axios": "^1.9.0",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"chart.js": "^4.4.9",
|
"chart.js": "^4.4.9",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@ -1771,6 +1773,37 @@
|
|||||||
"tslib": "^2.8.0"
|
"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": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"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==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/attr-accept": {
|
||||||
"version": "2.2.5",
|
"version": "2.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
|
||||||
@ -1971,6 +2009,16 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@ -2089,7 +2137,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
@ -2208,6 +2255,17 @@
|
|||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/commander": {
|
||||||
"version": "2.20.3",
|
"version": "2.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
"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==",
|
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@ -2370,7 +2436,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@ -2405,7 +2470,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
@ -2414,7 +2478,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
@ -2423,7 +2486,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
},
|
},
|
||||||
@ -2431,6 +2493,20 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.4",
|
"version": "0.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
||||||
@ -2875,6 +2951,58 @@
|
|||||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@ -2911,7 +3039,6 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
@ -2929,7 +3056,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
"es-define-property": "^1.0.1",
|
"es-define-property": "^1.0.1",
|
||||||
@ -2953,7 +3079,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dunder-proto": "^1.0.1",
|
"dunder-proto": "^1.0.1",
|
||||||
"es-object-atoms": "^1.0.0"
|
"es-object-atoms": "^1.0.0"
|
||||||
@ -2990,7 +3115,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
@ -3011,7 +3135,20 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
"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": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
@ -3023,7 +3160,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
},
|
},
|
||||||
@ -3564,7 +3700,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
@ -3937,6 +4072,11 @@
|
|||||||
"node": ">= 0.10"
|
"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": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
"@reduxjs/toolkit": "^2.8.1",
|
"@reduxjs/toolkit": "^2.8.1",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"axios": "^1.9.0",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"chart.js": "^4.4.9",
|
"chart.js": "^4.4.9",
|
||||||
"date-fns": "^4.1.0",
|
"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 { Send, X } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { selectCreator } from '../store/slices/creatorsSlice';
|
import { fetchCreatorDetail } from '../store/slices/creatorsSlice';
|
||||||
import { Button, Form } from 'react-bootstrap';
|
import { Button, Form } from 'react-bootstrap';
|
||||||
|
|
||||||
export default function ChatDetails({ onCloseChatDetails }) {
|
export default function ChatDetails({ onCloseChatDetails }) {
|
||||||
@ -16,7 +16,7 @@ export default function ChatDetails({ onCloseChatDetails }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(selectCreator(selectedChat.id));
|
dispatch(fetchCreatorDetail({ creatorId: selectedChat.id }));
|
||||||
}, [dispatch, selectedChat]);
|
}, [dispatch, selectedChat]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useRef, useCallback } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Table, Form, Spinner } from 'react-bootstrap';
|
import { Table, Form, Spinner } from 'react-bootstrap';
|
||||||
import { ChevronUp, ChevronDown } from 'lucide-react';
|
import { ChevronUp, ChevronDown } from 'lucide-react';
|
||||||
@ -7,27 +7,50 @@ import {
|
|||||||
toggleCreatorSelection,
|
toggleCreatorSelection,
|
||||||
selectAllCreators,
|
selectAllCreators,
|
||||||
clearCreatorSelection,
|
clearCreatorSelection,
|
||||||
|
fetchPrivateCreators,
|
||||||
|
setCreators,
|
||||||
|
resetCreators,
|
||||||
} from '../store/slices/creatorsSlice';
|
} from '../store/slices/creatorsSlice';
|
||||||
import { setSortBy } from '../store/slices/filtersSlice';
|
import { setSortBy } from '../store/slices/filtersSlice';
|
||||||
import '../styles/DatabaseList.scss';
|
import '../styles/DatabaseList.scss';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
export default function CreatorList({ path, pageType = 'database' }) {
|
export default function CreatorList({ path }) {
|
||||||
const dispatch = useDispatch();
|
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 { 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(() => {
|
useEffect(() => {
|
||||||
if (status === 'idle') {
|
dispatch(fetchCreators({ path, page: 1 }));
|
||||||
dispatch(fetchCreators({ path }));
|
}, [path, dispatch]);
|
||||||
}
|
|
||||||
}, [dispatch, status]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// console.log(creators);
|
}, [publicCreators]);
|
||||||
}, [creators]);
|
|
||||||
// 处理全选/取消全选
|
// 处理全选/取消全选
|
||||||
const handleSelectAll = (e) => {
|
const handleSelectAll = (e) => {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
@ -69,8 +92,8 @@ export default function CreatorList({ path, pageType = 'database' }) {
|
|||||||
return categoryMap[category] || '';
|
return categoryMap[category] || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果正在加载,显示加载中
|
// 如果正在加载且没有数据,显示加载中
|
||||||
if (status === 'loading') {
|
if (status === 'loading' && (!publicCreators || publicCreators.length === 0)) {
|
||||||
return (
|
return (
|
||||||
<div className='text-center p-5'>
|
<div className='text-center p-5'>
|
||||||
<Spinner animation='border' role='status' variant='primary'>
|
<Spinner animation='border' role='status' variant='primary'>
|
||||||
@ -87,13 +110,14 @@ export default function CreatorList({ path, pageType = 'database' }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='creator-database-table'>
|
<div className='creator-database-table'>
|
||||||
|
<div className='table-container'>
|
||||||
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden'>
|
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden'>
|
||||||
<thead>
|
<thead className='sticky-header'>
|
||||||
<tr>
|
<tr>
|
||||||
<th className='selector' style={{ width: '40px' }}>
|
<th className='selector' style={{ width: '40px' }}>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
type='checkbox'
|
type='checkbox'
|
||||||
checked={selectedCreators.length === creators.length && creators.length > 0}
|
checked={selectedCreators.length === publicCreators.length && publicCreators.length > 0}
|
||||||
onChange={handleSelectAll}
|
onChange={handleSelectAll}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
@ -107,11 +131,11 @@ export default function CreatorList({ path, pageType = 'database' }) {
|
|||||||
>
|
>
|
||||||
Category {renderSortIcon('category')}
|
Category {renderSortIcon('category')}
|
||||||
</th>
|
</th>
|
||||||
<th className='e-commerce-level text-center' onClick={() => handleSort('ecommerceLevel')}>
|
<th className='e-commerce-level text-center' onClick={() => handleSort('e_commerce_level')}>
|
||||||
E-commerce Level {renderSortIcon('ecommerceLevel')}
|
E-commerce Level {renderSortIcon('e_commerce_level')}
|
||||||
</th>
|
</th>
|
||||||
<th className='exposure-level text-center' onClick={() => handleSort('exposureLevel')}>
|
<th className='exposure-level text-center' onClick={() => handleSort('exposure_level')}>
|
||||||
Exposure Level {renderSortIcon('exposureLevel')}
|
Exposure Level {renderSortIcon('exposure_level')}
|
||||||
</th>
|
</th>
|
||||||
<th className='followers text-center' onClick={() => handleSort('followers')}>
|
<th className='followers text-center' onClick={() => handleSort('followers')}>
|
||||||
Followers {renderSortIcon('followers')}
|
Followers {renderSortIcon('followers')}
|
||||||
@ -119,44 +143,40 @@ export default function CreatorList({ path, pageType = 'database' }) {
|
|||||||
<th className='gmv text-center' onClick={() => handleSort('gmv')}>
|
<th className='gmv text-center' onClick={() => handleSort('gmv')}>
|
||||||
GMV {renderSortIcon('gmv')}
|
GMV {renderSortIcon('gmv')}
|
||||||
</th>
|
</th>
|
||||||
<th className='views text-center' onClick={() => handleSort('avgViews')}>
|
<th className='views text-center' onClick={() => handleSort('avg_video_views')}>
|
||||||
Avg. Video Views {renderSortIcon('avgViews')}
|
Avg. Video Views {renderSortIcon('avg_video_views')}
|
||||||
</th>
|
</th>
|
||||||
{pageType === 'private' && (
|
|
||||||
<>
|
|
||||||
<th className='pricing text-center'>Pricing</th>
|
|
||||||
<th className='collab-count text-center'># Collab</th>
|
|
||||||
<th className='latest-collab text-center'>Latest Collab.</th>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<th className='e-commerce text-center'>E-commerce</th>
|
<th className='e-commerce text-center'>E-commerce</th>
|
||||||
<th className='profile text-center'>Profile</th>
|
<th className='profile text-center'>Profile</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{creators.length === 0 ? (
|
{!publicCreators || publicCreators.length <= 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan='10' className='text-center py-4'>
|
<td colSpan='10' className='text-center py-4'>
|
||||||
No creators found matching your filters.
|
No creators found matching your filters.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
creators.map((creator) => (
|
publicCreators.map((creator) => (
|
||||||
<tr key={creator.id} className={selectedCreators.includes(creator.id) ? 'selected' : ''}>
|
<tr
|
||||||
|
key={creator.creator_id}
|
||||||
|
className={selectedCreators.includes(creator.creator_id) ? 'selected' : ''}
|
||||||
|
>
|
||||||
<td>
|
<td>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
type='checkbox'
|
type='checkbox'
|
||||||
checked={selectedCreators.includes(creator.id)}
|
checked={selectedCreators.includes(creator.creator_id)}
|
||||||
onChange={() => handleSelectCreator(creator.id)}
|
onChange={() => handleSelectCreator(creator.creator_id)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className='creator-cell'>
|
<td className='creator-cell'>
|
||||||
<div className='d-flex align-items-center'>
|
<div className='d-flex align-items-center'>
|
||||||
<div className='creator-avatar'>
|
<div className='creator-avatar'>
|
||||||
<img src={creator.avatar} alt={creator.name} />
|
<img src={creator.avatar} alt={creator.name} />
|
||||||
{creator.verified && <span className='verified-badge'></span>}
|
{creator.status && <span className='verified-badge'></span>}
|
||||||
</div>
|
</div>
|
||||||
<Link to={`/creator/${creator.id}`} className='creator-name'>
|
<Link to={`/creator/${creator.creator_id}`} className='creator-name'>
|
||||||
{creator.name}
|
{creator.name}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -167,11 +187,14 @@ export default function CreatorList({ path, pageType = 'database' }) {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className='text-center'>
|
<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>
|
||||||
<td className='text-center'>
|
<td className='text-center'>
|
||||||
<span className='level-badge exposure-level' data-level={creator.exposureLevel}>
|
<span
|
||||||
{creator.exposureLevel}
|
className='level-badge exposure-level'
|
||||||
|
data-level={creator.exposure_level}
|
||||||
|
>
|
||||||
|
{creator.exposure_level}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className='text-nowrap text-center'>{creator.followers}</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>{creator.gmv}</div>
|
||||||
<div className='small text-muted'>Items Sold: {creator.soldPercentage}</div>
|
<div className='small text-muted'>Items Sold: {creator.soldPercentage}</div>
|
||||||
</td>
|
</td>
|
||||||
<td className='text-nowrap text-center'>{creator.avgViews}</td>
|
<td className='text-nowrap text-center'>{creator.avg_video_views}</td>
|
||||||
{pageType === 'private' && (
|
|
||||||
<>
|
|
||||||
<td className='text-center'>{creator.pricing}</td>
|
|
||||||
<td className='text-center'>{creator.collabCount}</td>
|
|
||||||
<td className='text-center'>{creator.latestCollab}</td>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<td className='text-center'>
|
<td className='text-center'>
|
||||||
{creator.hasEcommerce ? <div className='colored-dot blue mx-auto'></div> : null}
|
{creator.hasEcommerce ? <div className='colored-dot blue mx-auto'></div> : null}
|
||||||
</td>
|
</td>
|
||||||
@ -202,6 +218,14 @@ export default function CreatorList({ path, pageType = 'database' }) {
|
|||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -78,8 +78,8 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
|||||||
|
|
||||||
// 组件加载时获取数据
|
// 组件加载时获取数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchCreators({ path }));
|
|
||||||
}, [dispatch, filters]);
|
}, [dispatch, filters, pageType, path]);
|
||||||
|
|
||||||
// 处理类别选择
|
// 处理类别选择
|
||||||
const handleCategorySelect = (category) => {
|
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 { createRoot } from 'react-dom/client';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
@ -7,9 +6,7 @@ import './index.css';
|
|||||||
import App from './App.jsx';
|
import App from './App.jsx';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<App />
|
<App />
|
||||||
</Provider>
|
</Provider>
|
||||||
</StrictMode>
|
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { Card, Table } from 'react-bootstrap';
|
import { Card, Table } from 'react-bootstrap';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
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 { Bar, Doughnut } from 'react-chartjs-2';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
@ -57,19 +57,67 @@ export default function CreatorDetail({}) {
|
|||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { selectedCreator } = useSelector((state) => state.creators);
|
const { selectedCreator, status } = useSelector((state) => state.creators);
|
||||||
const [activeTab, setActiveTab] = useState('basic');
|
const [activeTab, setActiveTab] = useState('basic');
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
navigate(-1);
|
navigate(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(selectCreator(id));
|
dispatch(fetchCreatorDetail({ creatorId: id }));
|
||||||
return () => {
|
return () => {
|
||||||
dispatch(clearCreator());
|
dispatch(clearCreator());
|
||||||
};
|
};
|
||||||
}, [dispatch, id]);
|
}, [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 (
|
return (
|
||||||
<div className='creator-detail-page'>
|
<div className='creator-detail-page'>
|
||||||
<div className='back-button' onClick={handleBack}>
|
<div className='back-button' onClick={handleBack}>
|
||||||
@ -80,35 +128,50 @@ export default function CreatorDetail({}) {
|
|||||||
<div className='creator-info-container card'>
|
<div className='creator-info-container card'>
|
||||||
<div className='creator-info-1'>
|
<div className='creator-info-1'>
|
||||||
<div className='creator-avatar'>
|
<div className='creator-avatar'>
|
||||||
<img src={selectedCreator.avatar} alt={selectedCreator.name} />
|
<img src={selectedCreator.creator.avatar} alt={selectedCreator.creator.name} />
|
||||||
</div>
|
</div>
|
||||||
<div className='creator-info-right'>
|
<div className='creator-info-right'>
|
||||||
<div className='creator-name'>{selectedCreator.name}</div>
|
<div className='creator-name'>{selectedCreator.creator.name}</div>
|
||||||
<div className='creator-desc'>{selectedCreator.description || '--'}</div>
|
<div className='creator-desc'>{selectedCreator?.description || '--'}</div>
|
||||||
<div className='creator-category'>{selectedCreator.category || '--'}</div>
|
<div className='creator-category'>{selectedCreator.business?.category || '--'}</div>
|
||||||
<div className='creator-location'>
|
<div className='creator-location'>
|
||||||
<MapPin size={16} />
|
<MapPin size={16} />
|
||||||
{selectedCreator.location || ' --'}
|
{selectedCreator.creator.location || ' --'}
|
||||||
</div>
|
</div>
|
||||||
<div className='creator-live-time'>{selectedCreator.liveTime || '--'}</div>
|
<div className='creator-live-time'>{selectedCreator.live_schedule || '--'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='creator-info-2'>
|
<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-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>
|
||||||
<div className='creator-info-item'>
|
<div className='creator-info-item'>
|
||||||
<div className='creator-info-label'>MCN</div>
|
<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>
|
||||||
<div className='creator-info-item'>
|
<div className='creator-info-item'>
|
||||||
<div className='creator-info-label'>Pricing</div>
|
<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>
|
||||||
<div className='creator-info-item'>
|
</div>
|
||||||
|
<div className='creator-info-item collab-info'>
|
||||||
<div className='creator-info-label'>Collab.</div>
|
<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>
|
</div>
|
||||||
<div className='creator-info-3'>
|
<div className='creator-info-3'>
|
||||||
@ -116,19 +179,23 @@ export default function CreatorDetail({}) {
|
|||||||
<div className='creator-info-label mail'>
|
<div className='creator-info-label mail'>
|
||||||
<Mail size={18} />
|
<Mail size={18} />
|
||||||
</div>
|
</div>
|
||||||
<div className='creator-info-value'>{selectedCreator.email || '--'}</div>
|
<div className='creator-info-value'>{selectedCreator.creator.email || '--'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='creator-info-item'>
|
<div className='creator-info-item'>
|
||||||
<div className='creator-info-label social'>
|
<div className='creator-info-label social'>
|
||||||
<Instagram size={18} />
|
<Instagram size={18} />
|
||||||
</div>
|
</div>
|
||||||
<div className='creator-info-value'>{selectedCreator.instagram || '--'}</div>
|
<div className='creator-info-value'>
|
||||||
|
{selectedCreator.creator.social_accounts?.instagram || '--'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='creator-info-item'>
|
<div className='creator-info-item'>
|
||||||
<div className='creator-info-label link'>
|
<div className='creator-info-label link'>
|
||||||
<Link size={18} />
|
<Link size={18} />
|
||||||
</div>
|
</div>
|
||||||
<div className='creator-info-value'>{selectedCreator.url || '--'}</div>
|
<div className='creator-info-value'>
|
||||||
|
{selectedCreator.creator.social_accounts?.tiktok || '--'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -136,47 +203,53 @@ export default function CreatorDetail({}) {
|
|||||||
<div className='levels'>
|
<div className='levels'>
|
||||||
<div className='level-item'>
|
<div className='level-item'>
|
||||||
<div className='name'>E-commerce Level</div>
|
<div className='name'>E-commerce Level</div>
|
||||||
<div className='value'>{selectedCreator.ecommerceLevel || '--'}</div>
|
<div className='value'>{selectedCreator?.metrics?.e_commerce_level || '--'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='level-item'>
|
<div className='level-item'>
|
||||||
<div className='name'>Exposure Level</div>
|
<div className='name'>Exposure Level</div>
|
||||||
<div className='value'>{selectedCreator.exposureLevel || '--'}</div>
|
<div className='value'>{selectedCreator?.metrics?.exposure_level || '--'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='data-cards'>
|
<div className='data-cards'>
|
||||||
<div className='data-card'>
|
<div className='data-card'>
|
||||||
<div className='value'>{selectedCreator.followers || '--'}</div>
|
<div className='value'>{selectedCreator?.metrics?.followers || '--'}</div>
|
||||||
<div className='name'>Followers</div>
|
<div className='name'>Followers</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='data-card'>
|
<div className='data-card'>
|
||||||
<div className='value'>{selectedCreator.gmv || '--'}</div>
|
<div className='value'>{selectedCreator?.metrics?.gmv || '--'}</div>
|
||||||
<div className='name'>GMV</div>
|
<div className='name'>GMV</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='data-card'>
|
<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 className='name'>Avg Video Views</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='data-card'>
|
<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 className='name'>Items Sold</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='data-card'>
|
<div className='data-card'>
|
||||||
<div className='value'>{selectedCreator.gpm || '--'}</div>
|
<div className='value'>{selectedCreator?.metrics?.gpm || '--'}</div>
|
||||||
<div className='name'>GPM</div>
|
<div className='name'>GPM</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='data-card'>
|
<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 className='name'>GMV per customer</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='data-charts'>
|
<div className='data-charts'>
|
||||||
<div className='data-chart'>
|
<div className='data-chart'>
|
||||||
<div className='chart-title'>GMV per sales channel</div>
|
<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>
|
||||||
<div className='data-chart'>
|
<div className='data-chart'>
|
||||||
<div className='chart-title'>GMV by product category</div>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -262,134 +335,134 @@ function CreatorBasicInfo({ selectedCreator }) {
|
|||||||
<div className='basic-info-list'>
|
<div className='basic-info-list'>
|
||||||
<div className='basic-info-title'>Collaboration Metrics</div>
|
<div className='basic-info-title'>Collaboration Metrics</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Avg. Commission Rate</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<div className='basic-info-item'>
|
||||||
<div className='value'>{selectedCreator.products || '--'}</div>
|
<div className='value'>{selectedCreator?.products || '--'}</div>
|
||||||
<div className='name'>Products</div>
|
<div className='name'>Products</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Brand Collaborations</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Product Price</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-list'>
|
<div className='basic-info-list'>
|
||||||
<div className='basic-info-title'>
|
<div className='basic-info-title'>
|
||||||
Video<span className='time-range'>{selectedCreator.videoTimeRange || '--'}</span>
|
Video<span className='time-range'>{selectedCreator?.videoTimeRange || '--'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<div className='basic-info-item'>
|
||||||
<div className='value'>{selectedCreator.avgVideoGpm || '--'}</div>
|
<div className='value'>{selectedCreator?.avgVideoGpm || '--'}</div>
|
||||||
<div className='name'>Video GPM</div>
|
<div className='name'>Video GPM</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Videos</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Avg. Video Views</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Avg. Video Engagement</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Avg. Video Likes</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-list'>
|
<div className='basic-info-list'>
|
||||||
<div className='basic-info-title'>
|
<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>
|
||||||
<div className='basic-info-item'>
|
<div className='basic-info-item'>
|
||||||
<div className='value'>{selectedCreator.avgVideoGpm || '--'}</div>
|
<div className='value'>{selectedCreator?.avgVideoGpm || '--'}</div>
|
||||||
<div className='name'>Video GPM</div>
|
<div className='name'>Video GPM</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Videos</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Avg. Video Views</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Avg. Video Engagement</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Avg. Video Likes</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-list'>
|
<div className='basic-info-list'>
|
||||||
<div className='basic-info-title'>
|
<div className='basic-info-title'>
|
||||||
LIVE<span className='time-range'>{selectedCreator.liveTimeRange || '--'}</span>
|
LIVE<span className='time-range'>{selectedCreator?.liveTimeRange || '--'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<div className='basic-info-item'>
|
||||||
<div className='value'>{selectedCreator.avgLiveGpm || '--'}</div>
|
<div className='value'>{selectedCreator?.avgLiveGpm || '--'}</div>
|
||||||
<div className='name'>LIVE GPM</div>
|
<div className='name'>LIVE GPM</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<div className='basic-info-item'>
|
||||||
<div className='value'>{selectedCreator.liveVideos || '--'}</div>
|
<div className='value'>{selectedCreator?.liveVideos || '--'}</div>
|
||||||
<div className='name'>LIVE Videos</div>
|
<div className='name'>LIVE Videos</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Avg. LIVE Views</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Avg. LIVE Engagement</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Avg. LIVE Likes</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-list'>
|
<div className='basic-info-list'>
|
||||||
<div className='basic-info-title'>
|
<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>
|
||||||
<div className='basic-info-item'>
|
<div className='basic-info-item'>
|
||||||
<div className='value'>{selectedCreator.avgLiveGpm || '--'}</div>
|
<div className='value'>{selectedCreator?.avgLiveGpm || '--'}</div>
|
||||||
<div className='name'>LIVE GPM</div>
|
<div className='name'>LIVE GPM</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<div className='basic-info-item'>
|
||||||
<div className='value'>{selectedCreator.liveVideos || '--'}</div>
|
<div className='value'>{selectedCreator?.liveVideos || '--'}</div>
|
||||||
<div className='name'>LIVE Videos</div>
|
<div className='name'>LIVE Videos</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Avg. LIVE Views</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Avg. LIVE Engagement</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-item'>
|
<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 className='name'>Avg. LIVE Likes</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='basic-info-list'>
|
<div className='basic-info-list'>
|
||||||
<div className='basic-info-title'>
|
<div className='basic-info-title'>
|
||||||
Follwers<span className='time-range'>{selectedCreator.liveTimeRange || '--'}</span>
|
Follwers<span className='time-range'>{selectedCreator?.liveTimeRange || '--'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='followers-data-charts'>
|
<div className='followers-data-charts'>
|
||||||
<div className='data-chart'>
|
<div className='data-chart'>
|
||||||
<div className='chart-title'>Follower Gender</div>
|
<div className='chart-title'>Follower Gender</div>
|
||||||
<Doughnut data={data} options={{ ...options, cutout: 90 }} />
|
<Doughnut data={data} options={{ ...options, cutout: 60 }} />
|
||||||
</div>
|
</div>
|
||||||
<div className='data-chart'>
|
<div className='data-chart'>
|
||||||
<div className='chart-title'>Follower Age</div>
|
<div className='chart-title'>Follower Age</div>
|
||||||
<Doughnut data={data} options={{ ...options, cutout: 90 }} />
|
<Doughnut data={data} options={{ ...options, cutout: 60 }} />
|
||||||
</div>
|
</div>
|
||||||
<div className='data-chart'>
|
<div className='data-chart'>
|
||||||
<div className='chart-title'>Top 5 Locations</div>
|
<div className='chart-title'>Top 5 Locations</div>
|
||||||
@ -399,7 +472,7 @@ function CreatorBasicInfo({ selectedCreator }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className='basic-info-list'>
|
<div className='basic-info-list'>
|
||||||
<div className='basic-info-title'>
|
<div className='basic-info-title'>
|
||||||
Trends<span className='time-range'>{selectedCreator.liveTimeRange || '--'}</span>
|
Trends<span className='time-range'>{selectedCreator?.liveTimeRange || '--'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='tab-switches'>
|
<div className='tab-switches'>
|
||||||
<div
|
<div
|
||||||
|
@ -5,7 +5,7 @@ import SearchBar from '../components/SearchBar';
|
|||||||
import { Button } from 'react-bootstrap';
|
import { Button } from 'react-bootstrap';
|
||||||
export default function Database({ path }) {
|
export default function Database({ path }) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<div className='database-page'>
|
||||||
<div className='function-bar'>
|
<div className='function-bar'>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
<Button>+ Add to Campaign</Button>
|
<Button>+ Add to Campaign</Button>
|
||||||
@ -17,7 +17,7 @@ export default function Database({ path }) {
|
|||||||
{path === 'youtube' && <div className='breadcrumb-item'>YouTube</div>}
|
{path === 'youtube' && <div className='breadcrumb-item'>YouTube</div>}
|
||||||
</div>
|
</div>
|
||||||
<DatabaseFilter path={path} pageType={'database'} />
|
<DatabaseFilter path={path} pageType={'database'} />
|
||||||
<CreatorList path={path} pageType={'database'} />
|
<CreatorList path={path} />
|
||||||
</React.Fragment>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,11 @@ import React from 'react';
|
|||||||
import SearchBar from '../components/SearchBar';
|
import SearchBar from '../components/SearchBar';
|
||||||
import { Button } from 'react-bootstrap';
|
import { Button } from 'react-bootstrap';
|
||||||
import DatabaseFilter from '../components/DatabaseFilter';
|
import DatabaseFilter from '../components/DatabaseFilter';
|
||||||
import CreatorList from '../components/CreatorList';
|
import PrivateCreatorList from '../components/PrivateCreatorList';
|
||||||
|
|
||||||
export default function PrivateCreator({ path }) {
|
export default function PrivateCreator({ path }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<div className='private-creator-page'>
|
||||||
<div className='function-bar'>
|
<div className='function-bar'>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
<Button>+ Add to Campaign</Button>
|
<Button>+ Add to Campaign</Button>
|
||||||
@ -19,7 +18,7 @@ export default function PrivateCreator({ path }) {
|
|||||||
{path === 'youtube' && <div className='breadcrumb-item'>YouTube</div>}
|
{path === 'youtube' && <div className='breadcrumb-item'>YouTube</div>}
|
||||||
</div>
|
</div>
|
||||||
<DatabaseFilter path={path} pageType={'private'} />
|
<DatabaseFilter path={path} pageType={'private'} />
|
||||||
<CreatorList path={path} pageType={'private'} />
|
<PrivateCreatorList path={path} />
|
||||||
</React.Fragment>
|
</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 { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import api from '../../services/api';
|
||||||
const mockVideos = [
|
const mockVideos = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -32,12 +33,12 @@ export const mockCreators = [
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=1',
|
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=1',
|
||||||
category: 'Phones & Electronics',
|
category: 'Phones & Electronics',
|
||||||
ecommerceLevel: 'L2',
|
e_commerce_level: 'L2',
|
||||||
exposureLevel: 'KOC-1',
|
exposure_level: 'KOC-1',
|
||||||
followers: '162.2k',
|
followers: '162.2k',
|
||||||
gmv: '$534.1k',
|
gmv: '$534.1k',
|
||||||
soldPercentage: '18.1%',
|
soldPercentage: '18.1%',
|
||||||
avgViews: '1.9k',
|
avg_video_views: '1.9k',
|
||||||
hasEcommerce: true,
|
hasEcommerce: true,
|
||||||
hasTiktok: true,
|
hasTiktok: true,
|
||||||
verified: true,
|
verified: true,
|
||||||
@ -49,12 +50,12 @@ export const mockCreators = [
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=2',
|
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=2',
|
||||||
category: 'Womenswear & Underwear',
|
category: 'Womenswear & Underwear',
|
||||||
ecommerceLevel: 'L3',
|
e_commerce_level: 'L3',
|
||||||
exposureLevel: 'KOL-3',
|
exposure_level: 'KOL-3',
|
||||||
followers: '162.2k',
|
followers: '162.2k',
|
||||||
gmv: '$534.1k',
|
gmv: '$534.1k',
|
||||||
soldPercentage: '18.1%',
|
soldPercentage: '18.1%',
|
||||||
avgViews: '1.9k',
|
avg_video_views: '1.9k',
|
||||||
hasEcommerce: false,
|
hasEcommerce: false,
|
||||||
hasTiktok: true,
|
hasTiktok: true,
|
||||||
verified: false,
|
verified: false,
|
||||||
@ -66,12 +67,12 @@ export const mockCreators = [
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=3',
|
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=3',
|
||||||
category: 'Sports & Outdoor',
|
category: 'Sports & Outdoor',
|
||||||
ecommerceLevel: 'L4',
|
e_commerce_level: 'L4',
|
||||||
exposureLevel: 'KOC-2',
|
exposure_level: 'KOC-2',
|
||||||
followers: '162.2k',
|
followers: '162.2k',
|
||||||
gmv: '$534.1k',
|
gmv: '$534.1k',
|
||||||
soldPercentage: '18.1%',
|
soldPercentage: '18.1%',
|
||||||
avgViews: '1.9k',
|
avg_video_views: '1.9k',
|
||||||
hasEcommerce: true,
|
hasEcommerce: true,
|
||||||
hasTiktok: true,
|
hasTiktok: true,
|
||||||
verified: false,
|
verified: false,
|
||||||
@ -83,12 +84,12 @@ export const mockCreators = [
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=4',
|
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=4',
|
||||||
category: 'Food & Beverage',
|
category: 'Food & Beverage',
|
||||||
ecommerceLevel: 'L1',
|
e_commerce_level: 'L1',
|
||||||
exposureLevel: 'KOC-2',
|
exposure_level: 'KOC-2',
|
||||||
followers: '162.2k',
|
followers: '162.2k',
|
||||||
gmv: '$534.1k',
|
gmv: '$534.1k',
|
||||||
soldPercentage: '18.1%',
|
soldPercentage: '18.1%',
|
||||||
avgViews: '1.9k',
|
avg_video_views: '1.9k',
|
||||||
hasEcommerce: true,
|
hasEcommerce: true,
|
||||||
hasTiktok: true,
|
hasTiktok: true,
|
||||||
hasInstagram: true,
|
hasInstagram: true,
|
||||||
@ -102,12 +103,12 @@ export const mockCreators = [
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=5',
|
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=5',
|
||||||
category: 'Health',
|
category: 'Health',
|
||||||
ecommerceLevel: 'L5',
|
e_commerce_level: 'L5',
|
||||||
exposureLevel: 'KOL-2',
|
exposure_level: 'KOL-2',
|
||||||
followers: '162.2k',
|
followers: '162.2k',
|
||||||
gmv: '$534.1k',
|
gmv: '$534.1k',
|
||||||
soldPercentage: '18.1%',
|
soldPercentage: '18.1%',
|
||||||
avgViews: '1.9k',
|
avg_video_views: '1.9k',
|
||||||
hasEcommerce: false,
|
hasEcommerce: false,
|
||||||
hasTiktok: true,
|
hasTiktok: true,
|
||||||
hasInstagram: true,
|
hasInstagram: true,
|
||||||
@ -121,12 +122,12 @@ export const mockCreators = [
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=6',
|
avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=6',
|
||||||
category: 'Kitchenware',
|
category: 'Kitchenware',
|
||||||
ecommerceLevel: 'New tag',
|
e_commerce_level: 'New tag',
|
||||||
exposureLevel: 'New tag',
|
exposure_level: 'New tag',
|
||||||
followers: '162.2k',
|
followers: '162.2k',
|
||||||
gmv: '$534.1k',
|
gmv: '$534.1k',
|
||||||
soldPercentage: '18.1%',
|
soldPercentage: '18.1%',
|
||||||
avgViews: '1.9k',
|
avg_video_views: '1.9k',
|
||||||
hasEcommerce: true,
|
hasEcommerce: true,
|
||||||
hasTiktok: true,
|
hasTiktok: true,
|
||||||
hasInstagram: true,
|
hasInstagram: true,
|
||||||
@ -138,66 +139,110 @@ export const mockCreators = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// 模拟API获取数据的异步Thunk
|
// 模拟API获取数据的异步Thunk
|
||||||
export const fetchCreators = createAsyncThunk('creators/fetchCreators', async ({ path }, { getState }) => {
|
// export const fetchCreators = createAsyncThunk('creators/fetchCreators', async ({ path }, { getState }) => {
|
||||||
// 模拟API调用延迟
|
// // 模拟API调用延迟
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
// await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
|
||||||
// 获取当前的筛选条件
|
// // 获取当前的筛选条件
|
||||||
const state = getState();
|
// const state = getState();
|
||||||
const filters = state.filters;
|
// const filters = state.filters;
|
||||||
|
|
||||||
// 应用筛选逻辑(实际项目中可能在服务器端进行)
|
// // 应用筛选逻辑(实际项目中可能在服务器端进行)
|
||||||
let filteredCreators = [...mockCreators];
|
// let filteredCreators = [...mockCreators];
|
||||||
|
|
||||||
// 如果有选定的类别,进行筛选
|
// // 如果有选定的类别,进行筛选
|
||||||
if (filters.category.length > 0) {
|
// if (filters.category.length > 0) {
|
||||||
filteredCreators = filteredCreators.filter((creator) => filters.category.includes(creator.category));
|
// filteredCreators = filteredCreators.filter((creator) => filters.category.includes(creator.category));
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 如果有选定的电商评级,进行筛选
|
// // 如果有选定的电商评级,进行筛选
|
||||||
if (filters.ecommerceRatings.length > 0) {
|
// if (filters.ecommerceRatings.length > 0) {
|
||||||
filteredCreators = filteredCreators.filter((creator) =>
|
// filteredCreators = filteredCreators.filter((creator) =>
|
||||||
filters.ecommerceRatings.includes(creator.ecommerceLevel)
|
// filters.ecommerceRatings.includes(creator.e_commerce_level)
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 如果有选定的曝光评级,进行筛选
|
// // 如果有选定的曝光评级,进行筛选
|
||||||
if (filters.exposureRatings.length > 0) {
|
// if (filters.exposureRatings.length > 0) {
|
||||||
filteredCreators = filteredCreators.filter((creator) =>
|
// filteredCreators = filteredCreators.filter((creator) =>
|
||||||
filters.exposureRatings.includes(creator.exposureLevel)
|
// filters.exposureRatings.includes(creator.exposure_level)
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 筛选观看量范围
|
// // 筛选观看量范围
|
||||||
if (filters.viewsRange.length === 2) {
|
// if (filters.viewsRange.length === 2) {
|
||||||
const minViews = filters.viewsRange[0];
|
// const minViews = filters.viewsRange[0];
|
||||||
const maxViews = filters.viewsRange[1];
|
// const maxViews = filters.viewsRange[1];
|
||||||
|
|
||||||
filteredCreators = filteredCreators.filter((creator) => {
|
// filteredCreators = filteredCreators.filter((creator) => {
|
||||||
// 将带k的字符串转换为数字
|
// // 将带k的字符串转换为数字
|
||||||
const viewsStr = creator.avgViews;
|
// const viewsStr = creator.avg_video_views;
|
||||||
let views = parseFloat(viewsStr);
|
// let views = parseFloat(viewsStr);
|
||||||
if (viewsStr.includes('k')) {
|
// if (viewsStr.includes('k')) {
|
||||||
views *= 1000;
|
// views *= 1000;
|
||||||
} else if (viewsStr.includes('M')) {
|
// } else if (viewsStr.includes('M')) {
|
||||||
views *= 1000000;
|
// views *= 1000000;
|
||||||
}
|
// }
|
||||||
|
|
||||||
return views >= minViews && views <= maxViews;
|
// return views >= minViews && views <= maxViews;
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
return filteredCreators;
|
// return filteredCreators;
|
||||||
});
|
// });
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
creators: [],
|
publicCreators: [],
|
||||||
|
privateCreators: [],
|
||||||
|
publicTiktokCreators: [],
|
||||||
|
publicInstagramCreators: [],
|
||||||
|
publicYoutubeCreators: [],
|
||||||
|
privateTiktokCreators: [],
|
||||||
|
privateInstagramCreators: [],
|
||||||
|
privateYoutubeCreators: [],
|
||||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||||
error: null,
|
error: null,
|
||||||
selectedCreators: [],
|
selectedCreators: [],
|
||||||
selectedCreator: null,
|
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({
|
const creatorsSlice = createSlice({
|
||||||
name: 'creators',
|
name: 'creators',
|
||||||
initialState,
|
initialState,
|
||||||
@ -218,32 +263,92 @@ const creatorsSlice = createSlice({
|
|||||||
clearCreatorSelection: (state) => {
|
clearCreatorSelection: (state) => {
|
||||||
state.selectedCreators = [];
|
state.selectedCreators = [];
|
||||||
},
|
},
|
||||||
selectCreator: (state, action) => {
|
|
||||||
const id = action.payload;
|
|
||||||
state.selectedCreator = state.creators.find((creator) => creator.id.toString() === id.toString());
|
|
||||||
},
|
|
||||||
clearCreator: (state) => {
|
clearCreator: (state) => {
|
||||||
state.selectedCreator = null;
|
state.selectedCreator = null;
|
||||||
},
|
},
|
||||||
|
resetCreators: (state) => {
|
||||||
|
state.publicCreators = [];
|
||||||
|
state.privateCreators = [];
|
||||||
|
state.pagination = initialState.pagination;
|
||||||
|
state.hasMore = true;
|
||||||
|
state.isLoadingMore = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder
|
builder
|
||||||
.addCase(fetchCreators.pending, (state) => {
|
.addCase(fetchCreators.pending, (state) => {
|
||||||
|
if (state.creators.length === 0) {
|
||||||
state.status = 'loading';
|
state.status = 'loading';
|
||||||
|
} else {
|
||||||
|
state.isLoadingMore = true;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.addCase(fetchCreators.fulfilled, (state, action) => {
|
.addCase(fetchCreators.fulfilled, (state, action) => {
|
||||||
state.status = 'succeeded';
|
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) => {
|
.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.status = 'failed';
|
||||||
state.error = action.error.message;
|
state.error = action.error.message;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { toggleCreatorSelection, selectAllCreators, clearCreatorSelection, selectCreator, clearCreator } =
|
export const {
|
||||||
creatorsSlice.actions;
|
toggleCreatorSelection,
|
||||||
|
selectAllCreators,
|
||||||
|
clearCreatorSelection,
|
||||||
|
clearCreator,
|
||||||
|
setCreators,
|
||||||
|
resetCreators,
|
||||||
|
} = creatorsSlice.actions;
|
||||||
|
|
||||||
export default creatorsSlice.reducer;
|
export default creatorsSlice.reducer;
|
||||||
|
@ -53,6 +53,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.creator-detail-page {
|
.creator-detail-page {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
gap: 1rem;
|
||||||
.creator-info-detail-container {
|
.creator-info-detail-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -78,6 +81,7 @@
|
|||||||
height: 115px;
|
height: 115px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background-color: $secondary-200;
|
||||||
}
|
}
|
||||||
.creator-info-right {
|
.creator-info-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -108,6 +112,28 @@
|
|||||||
.creator-info-value {
|
.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 {
|
.creator-info-3 {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -117,6 +143,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
color: $neutral-900;
|
color: $neutral-900;
|
||||||
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
.creator-info-label {
|
.creator-info-label {
|
||||||
@ -202,10 +229,10 @@
|
|||||||
color: $neutral-900;
|
color: $neutral-900;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
// canvas {
|
canvas {
|
||||||
// max-width: 260px;
|
max-width: 280px;
|
||||||
// max-height: 260px;
|
max-height: 200px;
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -267,6 +294,8 @@
|
|||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
canvas {
|
canvas {
|
||||||
|
max-width: 280px;
|
||||||
|
max-height: 200px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -290,7 +319,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-size: .875rem;
|
font-size: 0.875rem;
|
||||||
.video-title {
|
.video-title {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@ -312,7 +341,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,17 +3,28 @@
|
|||||||
// 导入变量
|
// 导入变量
|
||||||
@import './variables';
|
@import './variables';
|
||||||
|
|
||||||
|
.database-page {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.private-creator-page {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
.creator-database-table {
|
.creator-database-table {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.creator-cell {
|
.creator-cell {
|
||||||
.creator-avatar {
|
.creator-avatar {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
display: block;
|
||||||
height: 100%;
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border: 2px solid #e2e8f0;
|
border: 2px solid #e2e8f0;
|
||||||
@ -37,7 +48,8 @@
|
|||||||
|
|
||||||
.creator-name {
|
.creator-name {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #090909
|
color: #090909;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,4 +144,22 @@
|
|||||||
color: #000000;
|
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;
|
display: inline-flex !important;
|
||||||
align-items: center;
|
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;
|
transition: all 0.3s ease;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collapsed sidebar adjustments
|
// Collapsed sidebar adjustments
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
|
const env = loadEnv(mode, process.cwd());
|
||||||
|
return {
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
@ -23,4 +25,13 @@ export default defineConfig({
|
|||||||
cssCodeSplit: false,
|
cssCodeSplit: false,
|
||||||
minify: true,
|
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