diff --git a/.env b/.env new file mode 100644 index 0000000..ee02390 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +VITE_PROD = false +VITE_API_URL = "http://81.69.223.133:58099" \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..b5ea5b7 --- /dev/null +++ b/.env.development @@ -0,0 +1,2 @@ +VITE_PROD = false +VITE_API_URL = "http://81.69.223.133:58099" diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..ee02390 --- /dev/null +++ b/.env.production @@ -0,0 +1,2 @@ +VITE_PROD = false +VITE_API_URL = "http://81.69.223.133:58099" \ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index d155639..a9af679 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -81,12 +81,12 @@ OOIN Creator Center is a React application built with Vite that allows users to name: string, avatar: string, category: string, - ecommerceLevel: string, - exposureLevel: string, + e_commerce_level: string, + exposure_level: string, followers: string, gmv: string, soldPercentage: string, - avgViews: string, + avg_video_views: string, hasEcommerce: boolean, hasTiktok: boolean, hasInstagram: boolean, // optional diff --git a/package-lock.json b/package-lock.json index 7db4de0..0108d7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", "@reduxjs/toolkit": "^2.8.1", + "@tanstack/react-table": "^8.21.3", + "axios": "^1.9.0", "bootstrap": "^5.3.3", "chart.js": "^4.4.9", "date-fns": "^4.1.0", @@ -1771,6 +1773,37 @@ "tslib": "^2.8.0" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1963,6 +1996,11 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/attr-accept": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", @@ -1971,6 +2009,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2089,7 +2137,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -2208,6 +2255,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -2329,6 +2387,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2370,7 +2436,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -2405,7 +2470,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -2414,7 +2478,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -2423,7 +2486,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -2431,6 +2493,20 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", @@ -2875,6 +2951,58 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2911,7 +3039,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2929,7 +3056,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -2953,7 +3079,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -2990,7 +3115,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3011,7 +3135,20 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, "engines": { "node": ">= 0.4" }, @@ -3023,7 +3160,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -3564,7 +3700,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -3937,6 +4072,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index f62ac6c..e52f5f3 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", "@reduxjs/toolkit": "^2.8.1", + "@tanstack/react-table": "^8.21.3", + "axios": "^1.9.0", "bootstrap": "^5.3.3", "chart.js": "^4.4.9", "date-fns": "^4.1.0", diff --git a/src/assets/placeholder.png b/src/assets/placeholder.png new file mode 100644 index 0000000..444f317 Binary files /dev/null and b/src/assets/placeholder.png differ diff --git a/src/components/ChatDetails.jsx b/src/components/ChatDetails.jsx index d992712..379ca82 100644 --- a/src/components/ChatDetails.jsx +++ b/src/components/ChatDetails.jsx @@ -1,7 +1,7 @@ import { Send, X } from 'lucide-react'; import { useEffect, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { selectCreator } from '../store/slices/creatorsSlice'; +import { fetchCreatorDetail } from '../store/slices/creatorsSlice'; import { Button, Form } from 'react-bootstrap'; export default function ChatDetails({ onCloseChatDetails }) { @@ -16,7 +16,7 @@ export default function ChatDetails({ onCloseChatDetails }) { }; useEffect(() => { - dispatch(selectCreator(selectedChat.id)); + dispatch(fetchCreatorDetail({ creatorId: selectedChat.id })); }, [dispatch, selectedChat]); return ( diff --git a/src/components/CreatorList.jsx b/src/components/CreatorList.jsx index e6544f3..5903b62 100644 --- a/src/components/CreatorList.jsx +++ b/src/components/CreatorList.jsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Table, Form, Spinner } from 'react-bootstrap'; import { ChevronUp, ChevronDown } from 'lucide-react'; @@ -7,27 +7,50 @@ import { toggleCreatorSelection, selectAllCreators, clearCreatorSelection, + fetchPrivateCreators, + setCreators, + resetCreators, } from '../store/slices/creatorsSlice'; import { setSortBy } from '../store/slices/filtersSlice'; import '../styles/DatabaseList.scss'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Link } from 'react-router-dom'; -export default function CreatorList({ path, pageType = 'database' }) { +export default function CreatorList({ path }) { const dispatch = useDispatch(); - const { creators, status, selectedCreators } = useSelector((state) => state.creators); + const { + publicCreators = [], + status, + selectedCreators = [], + hasMore, + isLoadingMore, + pagination, + } = useSelector((state) => state.creators); const { sortBy, sortDirection } = useSelector((state) => state.filters); + const observer = useRef(); + const loadingRef = useCallback( + (node) => { + if (isLoadingMore) return; + if (observer.current) observer.current.disconnect(); + observer.current = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && hasMore) { + const nextPage = pagination?.current_page + 1; + dispatch(fetchCreators({ path, page: nextPage })); + } + }); + if (node) observer.current.observe(node); + }, + [isLoadingMore, hasMore, pagination, path, dispatch] + ); // 组件加载时获取创作者数据 useEffect(() => { - if (status === 'idle') { - dispatch(fetchCreators({ path })); - } - }, [dispatch, status]); + dispatch(fetchCreators({ path, page: 1 })); + }, [path, dispatch]); useEffect(() => { - // console.log(creators); - }, [creators]); + }, [publicCreators]); + // 处理全选/取消全选 const handleSelectAll = (e) => { if (e.target.checked) { @@ -69,8 +92,8 @@ export default function CreatorList({ path, pageType = 'database' }) { return categoryMap[category] || ''; }; - // 如果正在加载,显示加载中 - if (status === 'loading') { + // 如果正在加载且没有数据,显示加载中 + if (status === 'loading' && (!publicCreators || publicCreators.length === 0)) { return (
@@ -87,121 +110,122 @@ export default function CreatorList({ path, pageType = 'database' }) { return (
- - - - - - - - - - - - {pageType === 'private' && ( - <> - - - - - )} - - - - - - {creators.length === 0 ? ( +
+
- 0} - onChange={handleSelectAll} - /> - handleSort('name')} style={{ width: '180px' }}> - Creator {renderSortIcon('name')} - handleSort('category')} - style={{ width: '180px' }} - > - Category {renderSortIcon('category')} - handleSort('ecommerceLevel')}> - E-commerce Level {renderSortIcon('ecommerceLevel')} - handleSort('exposureLevel')}> - Exposure Level {renderSortIcon('exposureLevel')} - handleSort('followers')}> - Followers {renderSortIcon('followers')} - handleSort('gmv')}> - GMV {renderSortIcon('gmv')} - handleSort('avgViews')}> - Avg. Video Views {renderSortIcon('avgViews')} - Pricing# CollabLatest Collab.E-commerceProfile
+ - + + + + + + + + + + - ) : ( - creators.map((creator) => ( - - - - - - - - - - {pageType === 'private' && ( - <> - - - - - )} - - + + {!publicCreators || publicCreators.length <= 0 ? ( + + - )) - )} - -
- No creators found matching your filters. - + 0} + onChange={handleSelectAll} + /> + handleSort('name')} style={{ width: '180px' }}> + Creator {renderSortIcon('name')} + handleSort('category')} + style={{ width: '180px' }} + > + Category {renderSortIcon('category')} + handleSort('e_commerce_level')}> + E-commerce Level {renderSortIcon('e_commerce_level')} + handleSort('exposure_level')}> + Exposure Level {renderSortIcon('exposure_level')} + handleSort('followers')}> + Followers {renderSortIcon('followers')} + handleSort('gmv')}> + GMV {renderSortIcon('gmv')} + handleSort('avg_video_views')}> + Avg. Video Views {renderSortIcon('avg_video_views')} + E-commerceProfile
- handleSelectCreator(creator.id)} - /> - -
-
- {creator.name} - {creator.verified && } -
- - {creator.name} - -
-
- - {creator.category} - - - {creator.ecommerceLevel} - - - {creator.exposureLevel} - - {creator.followers} -
{creator.gmv}
-
Items Sold: {creator.soldPercentage}
-
{creator.avgViews}{creator.pricing}{creator.collabCount}{creator.latestCollab} - {creator.hasEcommerce ?
: null} -
- {creator.hasTiktok && ( -
- -
- )} +
+ No creators found matching your filters.
+ ) : ( + publicCreators.map((creator) => ( + + + handleSelectCreator(creator.public_id)} + /> + + +
+
+ {creator.name} + {creator.status && } +
+ + {creator.name} + +
+ + + + {creator.category} + + + + {creator.e_commerce_level} + + + + {creator.exposure_level} + + + {creator.followers} + +
{creator.gmv}
+
Items Sold: {creator.soldPercentage}
+ + {creator.avg_video_views} + + {creator.hasEcommerce ?
: null} + + + {creator.hasTiktok && ( +
+ +
+ )} + + + )) + )} + + + {hasMore && ( +
+ + Loading more... + +
+ )} +
); } diff --git a/src/components/DatabaseFilter.jsx b/src/components/DatabaseFilter.jsx index a0f4df5..02e8578 100644 --- a/src/components/DatabaseFilter.jsx +++ b/src/components/DatabaseFilter.jsx @@ -78,8 +78,8 @@ export default function DatabaseFilter({ path, pageType = 'database' }) { // 组件加载时获取数据 useEffect(() => { - dispatch(fetchCreators({ path })); - }, [dispatch, filters]); + + }, [dispatch, filters, pageType, path]); // 处理类别选择 const handleCategorySelect = (category) => { diff --git a/src/components/PrivateCreatorList.jsx b/src/components/PrivateCreatorList.jsx new file mode 100644 index 0000000..534822f --- /dev/null +++ b/src/components/PrivateCreatorList.jsx @@ -0,0 +1,230 @@ +import React, { useEffect, useRef, useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Table, Form, Spinner } from 'react-bootstrap'; +import { ChevronUp, ChevronDown } from 'lucide-react'; +import { + toggleCreatorSelection, + selectAllCreators, + clearCreatorSelection, + fetchPrivateCreators, +} from '../store/slices/creatorsSlice'; +import { setSortBy } from '../store/slices/filtersSlice'; +import '../styles/DatabaseList.scss'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Link } from 'react-router-dom'; + +export default function PrivateCreatorList({ path }) { + const dispatch = useDispatch(); + const { privateCreators, status, selectedCreators, hasMore, isLoadingMore, pagination } = useSelector( + (state) => state.creators + ); + const { sortBy, sortDirection } = useSelector((state) => state.filters); + const observer = useRef(); + const loadingRef = useCallback( + (node) => { + if (isLoadingMore) return; + if (observer.current) observer.current.disconnect(); + observer.current = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && hasMore) { + const nextPage = pagination?.current_page + 1; + dispatch(fetchPrivateCreators({ path, page: nextPage })); + } + }); + if (node) observer.current.observe(node); + }, + [isLoadingMore, hasMore, pagination, path, dispatch] + ); + + // 组件加载时获取创作者数据 + useEffect(() => { + dispatch(fetchPrivateCreators({ path, page: 1 })); + }, [path,dispatch]); + + useEffect(() => { + console.log(privateCreators); + }, [privateCreators]); + + // 处理全选/取消全选 + const handleSelectAll = (e) => { + if (e.target.checked) { + dispatch(selectAllCreators()); + } else { + dispatch(clearCreatorSelection()); + } + }; + + // 处理单个创作者选择 + const handleSelectCreator = (creatorId) => { + dispatch(toggleCreatorSelection(creatorId)); + }; + + // 处理排序 + const handleSort = (field) => { + dispatch(setSortBy(field)); + }; + + // 渲染排序图标 + const renderSortIcon = (field) => { + if (sortBy === field) { + return sortDirection === 'asc' ? : ; + } + return null; + }; + + // 根据类别获取样式类名 + const getCategoryClassName = (category) => { + const categoryMap = { + 'Phones & Electronics': 'phones', + 'Womenswear & Underwear': 'women', + 'Sports & Outdoor': 'sports', + 'Food & Beverage': 'food', + Health: 'health', + Kitchenware: 'kitchen', + }; + + return categoryMap[category] || ''; + }; + + // 如果正在加载且没有数据,显示加载中 + if (status === 'loading' && (!privateCreators || privateCreators.length === 0)) { + return ( +
+ + Loading... + +
+ ); + } + + // 如果加载失败,显示错误信息 + if (status === 'failed') { + return
Failed to load creators. Please try again later.
; + } + + return ( +
+
+ + + + + + + + + + + + + + + + + + + + {!privateCreators || privateCreators.length <= 0 ? ( + + + + ) : ( + privateCreators.map((creator) => ( + + + + + + + + + + + + + + + + )) + )} + +
+ 0} + onChange={handleSelectAll} + /> + handleSort('name')} style={{ width: '180px' }}> + Creator {renderSortIcon('name')} + handleSort('category')} + style={{ width: '180px' }} + > + Category {renderSortIcon('category')} + handleSort('e_commerce_level')}> + E-commerce Level {renderSortIcon('e_commerce_level')} + handleSort('exposure_level')}> + Exposure Level {renderSortIcon('exposure_level')} + handleSort('followers')}> + Followers {renderSortIcon('followers')} + handleSort('gmv')}> + GMV {renderSortIcon('gmv')} + handleSort('avg_video_views')}> + Avg. Video Views {renderSortIcon('avg_video_views')} + Pricing# CollabLatest Collab.E-commerceProfile
+ No creators found matching your filters. +
+ handleSelectCreator(creator.creator_id)} + /> + +
+
+ {creator.name} + {creator.status && } +
+ + {creator.name} + +
+
+ + {creator.category} + + + {creator.e_commerce_level} + + + {creator.exposure_level} + + {creator.followers} +
{creator.gmv}
+
Items Sold: {creator.soldPercentage}
+
{creator.avg_video_views}{creator.pricing}{creator.collab_count}{creator.latestCollab} + {creator.hasEcommerce ?
: null} +
+ {creator.hasTiktok && ( +
+ +
+ )} +
+ {hasMore && ( +
+ + Loading more... + +
+ )} +
+
+ ); +} diff --git a/src/main.jsx b/src/main.jsx index e8014f9..9e48fba 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,4 +1,3 @@ -import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import store from './store'; @@ -7,9 +6,7 @@ import './index.css'; import App from './App.jsx'; createRoot(document.getElementById('root')).render( - - - - - + + + ); diff --git a/src/pages/CreatorDetail.jsx b/src/pages/CreatorDetail.jsx index e8671d3..8f73578 100644 --- a/src/pages/CreatorDetail.jsx +++ b/src/pages/CreatorDetail.jsx @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'; import { Card, Table } from 'react-bootstrap'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; -import { selectCreator, clearCreator } from '../store/slices/creatorsSlice'; +import { clearCreator, fetchCreatorDetail } from '../store/slices/creatorsSlice'; import { Bar, Doughnut } from 'react-chartjs-2'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -57,19 +57,67 @@ export default function CreatorDetail({}) { const { id } = useParams(); const navigate = useNavigate(); const dispatch = useDispatch(); - const { selectedCreator } = useSelector((state) => state.creators); + const { selectedCreator, status } = useSelector((state) => state.creators); const [activeTab, setActiveTab] = useState('basic'); const handleBack = () => { navigate(-1); }; + useEffect(() => { - dispatch(selectCreator(id)); + dispatch(fetchCreatorDetail({ creatorId: id })); return () => { dispatch(clearCreator()); }; }, [dispatch, id]); - console.log(selectedCreator); + + const processChartData = (data, chart) => { + switch (chart) { + case 'channel': + return { + labels: Object.keys(data), + datasets: [ + { + label: 'GMV', + data: Object.values(data), + backgroundColor: [ + 'rgba(217, 107, 139)', + 'rgba(101, 105, 225)', + 'rgba(93, 200, 179)', + 'rgba(122, 87, 218)', + ], + }, + ], + }; + case 'category': + return { + labels: Object.keys(data), + datasets: [ + { + label: 'Sold', + data: Object.values(data), + backgroundColor: [ + 'rgba(217, 107, 139)', + 'rgba(101, 105, 225)', + 'rgba(93, 200, 179)', + 'rgba(122, 87, 218)', + ], + }, + ], + }; + } + }; + if (status === 'loading') { + return
Loading...
; + } + if (status === 'failed') { + return
Failed to load creator detail
; + } + + if (!selectedCreator) { + return
No creator found
; + } + return (
@@ -80,35 +128,50 @@ export default function CreatorDetail({}) {
- {selectedCreator.name} + {selectedCreator.creator.name}
-
{selectedCreator.name}
-
{selectedCreator.description || '--'}
-
{selectedCreator.category || '--'}
+
{selectedCreator.creator.name}
+
{selectedCreator?.description || '--'}
+
{selectedCreator.business?.category || '--'}
- {selectedCreator.location || ' --'} + {selectedCreator.creator.location || ' --'}
-
{selectedCreator.liveTime || '--'}
+
{selectedCreator.live_schedule || '--'}
-
+
Category
-
{selectedCreator.category}
+
+ {selectedCreator.business?.categories.map((item, index) => ( + + {item} + + ))} +
MCN
-
{selectedCreator.mcn || '--'}
+
{selectedCreator.business?.mcn || '--'}
Pricing
-
{selectedCreator.pricing}
+
+ {selectedCreator.business?.pricing?.range || '--'} +
-
+
Collab.
-
{selectedCreator.collab || '--'}
+
+ + {selectedCreator.business?.collab_count || '--'} + + + {selectedCreator.business?.latest_collab || '--'} + +
@@ -116,19 +179,23 @@ export default function CreatorDetail({}) {
-
{selectedCreator.email || '--'}
+
{selectedCreator.creator.email || '--'}
-
{selectedCreator.instagram || '--'}
+
+ {selectedCreator.creator.social_accounts?.instagram || '--'} +
-
{selectedCreator.url || '--'}
+
+ {selectedCreator.creator.social_accounts?.tiktok || '--'} +
@@ -136,47 +203,53 @@ export default function CreatorDetail({}) {
E-commerce Level
-
{selectedCreator.ecommerceLevel || '--'}
+
{selectedCreator?.metrics?.e_commerce_level || '--'}
Exposure Level
-
{selectedCreator.exposureLevel || '--'}
+
{selectedCreator?.metrics?.exposure_level || '--'}
-
{selectedCreator.followers || '--'}
+
{selectedCreator?.metrics?.followers || '--'}
Followers
-
{selectedCreator.gmv || '--'}
+
{selectedCreator?.metrics?.gmv || '--'}
GMV
-
{selectedCreator.avgVideoViews || '--'}
+
{selectedCreator?.metrics?.avg_video_views || '--'}
Avg Video Views
-
{selectedCreator.itemsSold || '--'}
+
{selectedCreator?.metrics?.items_sold || '--'}
Items Sold
-
{selectedCreator.gpm || '--'}
+
{selectedCreator?.metrics?.gpm || '--'}
GPM
-
{selectedCreator.gpmPerCustomer || '--'}
+
{selectedCreator?.metrics?.gmv_per_customer || '--'}
GMV per customer
GMV per sales channel
- +
GMV by product category
- +
@@ -262,134 +335,134 @@ function CreatorBasicInfo({ selectedCreator }) {
Collaboration Metrics
-
{selectedCreator.avgCommissionRate || '--'}
+
{selectedCreator?.avg_commission_rate || '--'}
Avg. Commission Rate
-
{selectedCreator.products || '--'}
+
{selectedCreator?.products || '--'}
Products
-
{selectedCreator.brandCollaborations || '--'}
+
{selectedCreator?.brand_collaborations || '--'}
Brand Collaborations
-
{selectedCreator.productPrice || '--'}
+
{selectedCreator?.product_price || '--'}
Product Price
- Video{selectedCreator.videoTimeRange || '--'} + Video{selectedCreator?.videoTimeRange || '--'}
-
{selectedCreator.avgVideoGpm || '--'}
+
{selectedCreator?.avgVideoGpm || '--'}
Video GPM
-
{selectedCreator.videos.length || '--'}
+
{selectedCreator?.videos?.length || '--'}
Videos
-
{selectedCreator.avgVideoViews || '--'}
+
{selectedCreator?.avgVideoViews || '--'}
Avg. Video Views
-
{selectedCreator.avgVideoEngagements || '--'}
+
{selectedCreator?.avgVideoEngagements || '--'}
Avg. Video Engagement
-
{selectedCreator.avgVideoLikes || '--'}
+
{selectedCreator?.avgVideoLikes || '--'}
Avg. Video Likes
- Shoppable Video{selectedCreator.videoTimeRange || '--'} + Shoppable Video{selectedCreator?.videoTimeRange || '--'}
-
{selectedCreator.avgVideoGpm || '--'}
+
{selectedCreator?.avgVideoGpm || '--'}
Video GPM
-
{selectedCreator.videos.length || '--'}
+
{selectedCreator?.videos?.length || '--'}
Videos
-
{selectedCreator.avgVideoViews || '--'}
+
{selectedCreator?.avgVideoViews || '--'}
Avg. Video Views
-
{selectedCreator.avgVideoEngagements || '--'}
+
{selectedCreator?.avgVideoEngagements || '--'}
Avg. Video Engagement
-
{selectedCreator.avgVideoLikes || '--'}
+
{selectedCreator?.avgVideoLikes || '--'}
Avg. Video Likes
- LIVE{selectedCreator.liveTimeRange || '--'} + LIVE{selectedCreator?.liveTimeRange || '--'}
-
{selectedCreator.avgLiveGpm || '--'}
+
{selectedCreator?.avgLiveGpm || '--'}
LIVE GPM
-
{selectedCreator.liveVideos || '--'}
+
{selectedCreator?.liveVideos || '--'}
LIVE Videos
-
{selectedCreator.avgLiveViews || '--'}
+
{selectedCreator?.avgLiveViews || '--'}
Avg. LIVE Views
-
{selectedCreator.avgLiveEngagements || '--'}
+
{selectedCreator?.avgLiveEngagements || '--'}
Avg. LIVE Engagement
-
{selectedCreator.avgLiveLikes || '--'}
+
{selectedCreator?.avgLiveLikes || '--'}
Avg. LIVE Likes
- Shoppable LIVE{selectedCreator.liveTimeRange || '--'} + Shoppable LIVE{selectedCreator?.liveTimeRange || '--'}
-
{selectedCreator.avgLiveGpm || '--'}
+
{selectedCreator?.avgLiveGpm || '--'}
LIVE GPM
-
{selectedCreator.liveVideos || '--'}
+
{selectedCreator?.liveVideos || '--'}
LIVE Videos
-
{selectedCreator.avgLiveViews || '--'}
+
{selectedCreator?.avgLiveViews || '--'}
Avg. LIVE Views
-
{selectedCreator.avgLiveEngagements || '--'}
+
{selectedCreator?.avgLiveEngagements || '--'}
Avg. LIVE Engagement
-
{selectedCreator.avgLiveLikes || '--'}
+
{selectedCreator?.avgLiveLikes || '--'}
Avg. LIVE Likes
- Follwers{selectedCreator.liveTimeRange || '--'} + Follwers{selectedCreator?.liveTimeRange || '--'}
Follower Gender
- +
Follower Age
- +
Top 5 Locations
@@ -399,7 +472,7 @@ function CreatorBasicInfo({ selectedCreator }) {
- Trends{selectedCreator.liveTimeRange || '--'} + Trends{selectedCreator?.liveTimeRange || '--'}
+
@@ -17,7 +17,7 @@ export default function Database({ path }) { {path === 'youtube' &&
YouTube
}
- - + +
); } diff --git a/src/pages/PrivateCreator.jsx b/src/pages/PrivateCreator.jsx index f87d371..f20ba87 100644 --- a/src/pages/PrivateCreator.jsx +++ b/src/pages/PrivateCreator.jsx @@ -2,24 +2,23 @@ import React from 'react'; import SearchBar from '../components/SearchBar'; import { Button } from 'react-bootstrap'; import DatabaseFilter from '../components/DatabaseFilter'; -import CreatorList from '../components/CreatorList'; +import PrivateCreatorList from '../components/PrivateCreatorList'; export default function PrivateCreator({ path }) { - return ( - -
- - -
-
-
Private Creators
- {path === 'tiktok' &&
TikTok
} - {path === 'instagram' &&
Instagram
} - {path === 'youtube' &&
YouTube
} -
- - -
+
+
+ + +
+
+
Private Creators
+ {path === 'tiktok' &&
TikTok
} + {path === 'instagram' &&
Instagram
} + {path === 'youtube' &&
YouTube
} +
+ + +
); } diff --git a/src/services/api.js b/src/services/api.js new file mode 100644 index 0000000..50fac2c --- /dev/null +++ b/src/services/api.js @@ -0,0 +1,61 @@ +import axios from 'axios'; + +const api = axios.create({ + baseURL: '/api', + withCredentials: true, // Include cookies if needed +}); + +api.interceptors.request.use( + (config) => { + const token = sessionStorage.getItem('token') || '03d9163150a8bfbbee2a33c4444237f337a35278'; + if (token) { + config.headers.Authorization = `Token ${token}`; + } + return config; + }, + (error) => { + console.error('Request error:', error); + return Promise.reject(error); + } +); + +api.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + return Promise.reject(error); + } +); + +const get = async (url, params) => { + const response = await api.get(url, params); + return response.data; +}; + +const post = async (url, data) => { + const response = await api.post(url, data); + return response.data; +}; + +const put = async (url, data) => { + const response = await api.put(url, data); + return response.data; +}; + +const del = async (url) => { + const response = await api.delete(url); + return response.data; +}; + +const upload = async (url, data) => { + const response = await api.post(url, data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + return response.data; +}; + + +export default { get, post, put, del, upload }; diff --git a/src/store/slices/creatorsSlice.js b/src/store/slices/creatorsSlice.js index 302be93..76c3ea4 100644 --- a/src/store/slices/creatorsSlice.js +++ b/src/store/slices/creatorsSlice.js @@ -1,4 +1,5 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import api from '../../services/api'; const mockVideos = [ { id: 1, @@ -32,12 +33,12 @@ export const mockCreators = [ name: 'name', avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=1', category: 'Phones & Electronics', - ecommerceLevel: 'L2', - exposureLevel: 'KOC-1', + e_commerce_level: 'L2', + exposure_level: 'KOC-1', followers: '162.2k', gmv: '$534.1k', soldPercentage: '18.1%', - avgViews: '1.9k', + avg_video_views: '1.9k', hasEcommerce: true, hasTiktok: true, verified: true, @@ -49,12 +50,12 @@ export const mockCreators = [ name: 'name', avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=2', category: 'Womenswear & Underwear', - ecommerceLevel: 'L3', - exposureLevel: 'KOL-3', + e_commerce_level: 'L3', + exposure_level: 'KOL-3', followers: '162.2k', gmv: '$534.1k', soldPercentage: '18.1%', - avgViews: '1.9k', + avg_video_views: '1.9k', hasEcommerce: false, hasTiktok: true, verified: false, @@ -66,12 +67,12 @@ export const mockCreators = [ name: 'name', avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=3', category: 'Sports & Outdoor', - ecommerceLevel: 'L4', - exposureLevel: 'KOC-2', + e_commerce_level: 'L4', + exposure_level: 'KOC-2', followers: '162.2k', gmv: '$534.1k', soldPercentage: '18.1%', - avgViews: '1.9k', + avg_video_views: '1.9k', hasEcommerce: true, hasTiktok: true, verified: false, @@ -83,12 +84,12 @@ export const mockCreators = [ name: 'name', avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=4', category: 'Food & Beverage', - ecommerceLevel: 'L1', - exposureLevel: 'KOC-2', + e_commerce_level: 'L1', + exposure_level: 'KOC-2', followers: '162.2k', gmv: '$534.1k', soldPercentage: '18.1%', - avgViews: '1.9k', + avg_video_views: '1.9k', hasEcommerce: true, hasTiktok: true, hasInstagram: true, @@ -102,12 +103,12 @@ export const mockCreators = [ name: 'name', avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=5', category: 'Health', - ecommerceLevel: 'L5', - exposureLevel: 'KOL-2', + e_commerce_level: 'L5', + exposure_level: 'KOL-2', followers: '162.2k', gmv: '$534.1k', soldPercentage: '18.1%', - avgViews: '1.9k', + avg_video_views: '1.9k', hasEcommerce: false, hasTiktok: true, hasInstagram: true, @@ -121,12 +122,12 @@ export const mockCreators = [ name: 'name', avatar: 'https://api.dicebear.com/7.x/micah/svg?seed=6', category: 'Kitchenware', - ecommerceLevel: 'New tag', - exposureLevel: 'New tag', + e_commerce_level: 'New tag', + exposure_level: 'New tag', followers: '162.2k', gmv: '$534.1k', soldPercentage: '18.1%', - avgViews: '1.9k', + avg_video_views: '1.9k', hasEcommerce: true, hasTiktok: true, hasInstagram: true, @@ -138,66 +139,110 @@ export const mockCreators = [ ]; // 模拟API获取数据的异步Thunk -export const fetchCreators = createAsyncThunk('creators/fetchCreators', async ({ path }, { getState }) => { - // 模拟API调用延迟 - await new Promise((resolve) => setTimeout(resolve, 500)); +// export const fetchCreators = createAsyncThunk('creators/fetchCreators', async ({ path }, { getState }) => { +// // 模拟API调用延迟 +// await new Promise((resolve) => setTimeout(resolve, 500)); - // 获取当前的筛选条件 - const state = getState(); - const filters = state.filters; +// // 获取当前的筛选条件 +// const state = getState(); +// const filters = state.filters; - // 应用筛选逻辑(实际项目中可能在服务器端进行) - let filteredCreators = [...mockCreators]; +// // 应用筛选逻辑(实际项目中可能在服务器端进行) +// let filteredCreators = [...mockCreators]; - // 如果有选定的类别,进行筛选 - if (filters.category.length > 0) { - filteredCreators = filteredCreators.filter((creator) => filters.category.includes(creator.category)); - } +// // 如果有选定的类别,进行筛选 +// if (filters.category.length > 0) { +// filteredCreators = filteredCreators.filter((creator) => filters.category.includes(creator.category)); +// } - // 如果有选定的电商评级,进行筛选 - if (filters.ecommerceRatings.length > 0) { - filteredCreators = filteredCreators.filter((creator) => - filters.ecommerceRatings.includes(creator.ecommerceLevel) - ); - } +// // 如果有选定的电商评级,进行筛选 +// if (filters.ecommerceRatings.length > 0) { +// filteredCreators = filteredCreators.filter((creator) => +// filters.ecommerceRatings.includes(creator.e_commerce_level) +// ); +// } - // 如果有选定的曝光评级,进行筛选 - if (filters.exposureRatings.length > 0) { - filteredCreators = filteredCreators.filter((creator) => - filters.exposureRatings.includes(creator.exposureLevel) - ); - } +// // 如果有选定的曝光评级,进行筛选 +// if (filters.exposureRatings.length > 0) { +// filteredCreators = filteredCreators.filter((creator) => +// filters.exposureRatings.includes(creator.exposure_level) +// ); +// } - // 筛选观看量范围 - if (filters.viewsRange.length === 2) { - const minViews = filters.viewsRange[0]; - const maxViews = filters.viewsRange[1]; +// // 筛选观看量范围 +// if (filters.viewsRange.length === 2) { +// const minViews = filters.viewsRange[0]; +// const maxViews = filters.viewsRange[1]; - filteredCreators = filteredCreators.filter((creator) => { - // 将带k的字符串转换为数字 - const viewsStr = creator.avgViews; - let views = parseFloat(viewsStr); - if (viewsStr.includes('k')) { - views *= 1000; - } else if (viewsStr.includes('M')) { - views *= 1000000; - } +// filteredCreators = filteredCreators.filter((creator) => { +// // 将带k的字符串转换为数字 +// const viewsStr = creator.avg_video_views; +// let views = parseFloat(viewsStr); +// if (viewsStr.includes('k')) { +// views *= 1000; +// } else if (viewsStr.includes('M')) { +// views *= 1000000; +// } - return views >= minViews && views <= maxViews; - }); - } +// return views >= minViews && views <= maxViews; +// }); +// } - return filteredCreators; -}); +// return filteredCreators; +// }); const initialState = { - creators: [], + publicCreators: [], + privateCreators: [], + publicTiktokCreators: [], + publicInstagramCreators: [], + publicYoutubeCreators: [], + privateTiktokCreators: [], + privateInstagramCreators: [], + privateYoutubeCreators: [], status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' error: null, selectedCreators: [], selectedCreator: null, + pagination: { + current_page: 1, + total_pages: 0, + total_count: 0, + has_next: false, + has_prev: false, + }, + hasMore: true, + isLoadingMore: false, }; +export const fetchCreators = createAsyncThunk('creators/fetchCreators', async ({ path, page = 1 }, { getState }) => { + const state = getState(); + const filters = state.filters; + + const response = await api.get(`/daren_detail/public/creators`, { params: { page } }); + return response; +}); + +export const fetchPrivateCreators = createAsyncThunk( + 'creators/fetchPrivateCreators', + async ({ path, page = 1 }, { getState }) => { + const state = getState(); + const filters = state.filters; + + const queryParams = { pool_id: 1, page }; + const response = await api.get(`/daren_detail/private/pools/creators`, { params: queryParams }); + return response; + } +); + +export const fetchCreatorDetail = createAsyncThunk( + 'creators/fetchCreatorDetail', + async ({ creatorId }, { getState }) => { + const response = await api.get(`/daren_detail/creators/${creatorId}`); + return response; + } +); + const creatorsSlice = createSlice({ name: 'creators', initialState, @@ -218,32 +263,92 @@ const creatorsSlice = createSlice({ clearCreatorSelection: (state) => { state.selectedCreators = []; }, - selectCreator: (state, action) => { - const id = action.payload; - state.selectedCreator = state.creators.find((creator) => creator.id.toString() === id.toString()); - }, clearCreator: (state) => { state.selectedCreator = null; }, + resetCreators: (state) => { + state.publicCreators = []; + state.privateCreators = []; + state.pagination = initialState.pagination; + state.hasMore = true; + state.isLoadingMore = false; + }, }, extraReducers: (builder) => { builder .addCase(fetchCreators.pending, (state) => { - state.status = 'loading'; + if (state.creators.length === 0) { + state.status = 'loading'; + } else { + state.isLoadingMore = true; + } }) .addCase(fetchCreators.fulfilled, (state, action) => { state.status = 'succeeded'; - state.creators = action.payload; + state.isLoadingMore = false; + const { data, pagination } = action.payload; + + if (pagination.current_page === 1) { + state.publicCreators = data; + } else { + state.publicCreators = [...state.publicCreators, ...data]; + } + state.pagination = pagination; + state.hasMore = pagination.has_next; }) .addCase(fetchCreators.rejected, (state, action) => { - console.log(action); + state.status = 'failed'; + state.isLoadingMore = false; + state.error = action.error.message; + }) + .addCase(fetchPrivateCreators.pending, (state) => { + if (state.creators?.length === 0) { + state.status = 'loading'; + } else { + state.isLoadingMore = true; + } + }) + .addCase(fetchPrivateCreators.fulfilled, (state, action) => { + state.status = 'succeeded'; + state.isLoadingMore = false; + const { data, pagination } = action.payload; + + if (pagination.current_page === 1) { + state.privateCreators = data; + } else { + state.privateCreators = [...state.privateCreators, ...data]; + } + state.pagination = pagination; + state.hasMore = pagination.has_next; + }) + .addCase(fetchPrivateCreators.rejected, (state, action) => { + console.log('fetchPrivateCreators.rejected', action); + state.status = 'failed'; + state.isLoadingMore = false; + state.error = action.error.message; + }) + .addCase(fetchCreatorDetail.pending, (state) => { + state.status = 'loading'; + }) + .addCase(fetchCreatorDetail.fulfilled, (state, action) => { + state.status = 'succeeded'; + const { data, pagination } = action.payload; + state.selectedCreator = data; + }) + .addCase(fetchCreatorDetail.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; }); }, }); -export const { toggleCreatorSelection, selectAllCreators, clearCreatorSelection, selectCreator, clearCreator } = - creatorsSlice.actions; +export const { + toggleCreatorSelection, + selectAllCreators, + clearCreatorSelection, + clearCreator, + setCreators, + resetCreators, +} = creatorsSlice.actions; export default creatorsSlice.reducer; diff --git a/src/styles/CreatorDiscovery.scss b/src/styles/CreatorDiscovery.scss index 802f161..504d786 100644 --- a/src/styles/CreatorDiscovery.scss +++ b/src/styles/CreatorDiscovery.scss @@ -53,6 +53,9 @@ } .creator-detail-page { + display: flex; + flex-flow: column nowrap; + gap: 1rem; .creator-info-detail-container { display: flex; flex-direction: row; @@ -78,6 +81,7 @@ height: 115px; border-radius: 50%; overflow: hidden; + background-color: $secondary-200; } .creator-info-right { display: flex; @@ -108,6 +112,28 @@ .creator-info-value { } } + .category-info { + .creator-info-value { + display: flex; + flex-flow: row nowrap; + gap: 0.5rem; + } + } + .collab-info { + .creator-info-value { + display: flex; + flex-flow: row nowrap; + gap: 0.5rem; + } + } + .badge { + padding: 6px 8px; + border-radius: 12px; + color: $neutral-600; + background-color: $neutral-200; + font-size: 0.875rem; + font-weight: 500; + } } .creator-info-3 { display: flex; @@ -117,6 +143,7 @@ display: flex; flex-flow: row nowrap; color: $neutral-900; + align-items: center; gap: 0.5rem; .creator-info-label { @@ -202,10 +229,10 @@ color: $neutral-900; margin-bottom: 1rem; } - // canvas { - // max-width: 260px; - // max-height: 260px; - // } + canvas { + max-width: 280px; + max-height: 200px; + } } } } @@ -267,6 +294,8 @@ flex-flow: column nowrap; justify-content: space-between; canvas { + max-width: 280px; + max-height: 200px; } } } @@ -290,7 +319,7 @@ display: flex; flex-flow: column nowrap; gap: 0.5rem; - font-size: .875rem; + font-size: 0.875rem; .video-title { font-weight: 700; text-overflow: ellipsis; @@ -312,7 +341,6 @@ align-items: center; } } - } } } diff --git a/src/styles/DatabaseList.scss b/src/styles/DatabaseList.scss index fcb1d00..b30be13 100644 --- a/src/styles/DatabaseList.scss +++ b/src/styles/DatabaseList.scss @@ -3,17 +3,28 @@ // 导入变量 @import './variables'; +.database-page { + height: 100%; + position: relative; +} +.private-creator-page { + height: 100%; + position: relative; +} .creator-database-table { + height: 100%; + position: relative; + .creator-cell { .creator-avatar { position: relative; - width: 36px; - height: 36px; margin-right: 12px; + flex-shrink: 0; img { - width: 100%; - height: 100%; + display: block; + width: 36px; + height: 36px; border-radius: 50%; object-fit: cover; border: 2px solid #e2e8f0; @@ -37,7 +48,8 @@ .creator-name { font-weight: 700; - color: #090909 + color: #090909; + white-space: nowrap; } } @@ -132,4 +144,22 @@ color: #000000; } } + + .table-container { + position: relative; + max-height: calc(100% - 455px); // Adjust this value based on your layout + overflow-y: auto; + + .sticky-header { + position: sticky; + top: 0; + z-index: 1; + background: white; + + th { + background: white; + border-bottom: 2px solid #dee2e6; + } + } + } } diff --git a/src/styles/global.scss b/src/styles/global.scss index 52d8502..598c310 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -31,4 +31,11 @@ .btn { display: inline-flex !important; align-items: center; +} +.back-button { + width: max-content; + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; } \ No newline at end of file diff --git a/src/styles/sidebar.scss b/src/styles/sidebar.scss index 2062263..7283c26 100644 --- a/src/styles/sidebar.scss +++ b/src/styles/sidebar.scss @@ -151,6 +151,7 @@ transition: all 0.3s ease; background: #f8f9fa; border-radius: 8px; + overflow: hidden; } // Collapsed sidebar adjustments diff --git a/vite.config.js b/vite.config.js index 531f180..484893d 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,26 +1,37 @@ -import { defineConfig } from 'vite'; +import { defineConfig, loadEnv } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'path'; // https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, - css: { - preprocessorOptions: { - scss: { - quietDeps: true, - outputStyle: 'compressed', +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd()); + return { + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), }, }, - devSourcemap: false, - }, - build: { - cssCodeSplit: false, - minify: true, - }, + css: { + preprocessorOptions: { + scss: { + quietDeps: true, + outputStyle: 'compressed', + }, + }, + devSourcemap: false, + }, + build: { + cssCodeSplit: false, + minify: true, + }, + server: { + proxy: { + '/api': { + target: env.VITE_API_URL || 'http://81.69.223.133:8008', + changeOrigin: true, + }, + }, + }, + }; });