mirror of
https://github.com/Funkoala14/CreatorCenter_OOIN.git
synced 2025-06-07 12:28:15 +08:00
[dev]discovery render
This commit is contained in:
parent
6953882a0f
commit
63dfd70117
@ -25,7 +25,7 @@ export default function BrandsList({ openBrandDetail }) {
|
||||
|
||||
return (
|
||||
<div className='brands-list'>
|
||||
{brands?.length > 0 && brands.map((brand) => (
|
||||
{brands?.length > 0 ? brands.map((brand) => (
|
||||
<div className='brand-card shadow-xs' key={brand.id} onClick={() => openBrandDetail(brand)}>
|
||||
<Card.Body>
|
||||
<Card.Title className='text-primary fw-bold'>
|
||||
@ -62,7 +62,9 @@ export default function BrandsList({ openBrandDetail }) {
|
||||
</Card.Text>
|
||||
</Card.Body>
|
||||
</div>
|
||||
))}
|
||||
)) : (
|
||||
<div className='alert alert-info'>No brands found.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -176,17 +176,17 @@ export default function CreatorList({ path }) {
|
||||
/>
|
||||
</td>
|
||||
<td className='creator-cell'>
|
||||
<div className='d-flex align-items-center'>
|
||||
<Link to={`/creator/${creator.creator_id}`} 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'>
|
||||
<div className='creator-name'>
|
||||
{creator.name}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<td className='text-center'>
|
||||
<span className={`category-pill ${getCategoryClassName(creator.category)}`}>
|
||||
{creator.category}
|
||||
</span>
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Table } from 'react-bootstrap';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function DiscoveryList() {
|
||||
const dispatch = useDispatch();
|
||||
const { creators, error, status } = useSelector((state) => state.discovery);
|
||||
const { sessions, error, status } = useSelector((state) => state.discovery);
|
||||
|
||||
// 如果加载失败,显示错误信息
|
||||
if (status === 'failed') {
|
||||
@ -27,18 +28,18 @@ export default function DiscoveryList() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{creators?.length > 0 ? (
|
||||
creators.map((creator) => (
|
||||
<tr key={creator.id}>
|
||||
<td className='text-center'>{creator.sessions}</td>
|
||||
<td className='text-center'>{creator.creator}</td>
|
||||
<td className='text-center'>{creator.shoppableCreators}</td>
|
||||
<td className='text-center'>{creator.avgFollowers}</td>
|
||||
<td className='text-center'>{creator.avgGMV}</td>
|
||||
<td className='text-center'>{creator.avgVideoViews}</td>
|
||||
<td className='text-center'>{creator.date}</td>
|
||||
{sessions?.length > 0 ? (
|
||||
sessions.map((session) => (
|
||||
<tr key={session.id}>
|
||||
<td className='text-center'>{session.session_number}</td>
|
||||
<td className='text-center'>{session.creator_count}</td>
|
||||
<td className='text-center'>{session.shoppable_creators}</td>
|
||||
<td className='text-center'>{session.avg_followers}</td>
|
||||
<td className='text-center'>{session.avg_gmv}</td>
|
||||
<td className='text-center'>{session.avg_video_views}</td>
|
||||
<td className='text-center'>{session.date_created}</td>
|
||||
<td className='text-center'>
|
||||
<Link to={`/creator/${creator.id}`}>View</Link>
|
||||
<Link to={`/creator/${session.id}`}>View</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
@ -54,3 +55,103 @@ export default function DiscoveryList() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SeesionResultModal({ session }) {
|
||||
|
||||
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='creator' style={{ width: '180px' }}>
|
||||
Creator
|
||||
</th>
|
||||
<th
|
||||
className='category text-center'
|
||||
style={{ width: '180px' }}
|
||||
>
|
||||
Category
|
||||
</th>
|
||||
<th className='e-commerce-level text-center'>
|
||||
E-commerce Level
|
||||
</th>
|
||||
<th className='exposure-level text-center'>
|
||||
Exposure Level
|
||||
</th>
|
||||
<th className='followers text-center'>
|
||||
Followers
|
||||
</th>
|
||||
<th className='gmv text-center'>
|
||||
GMV
|
||||
</th>
|
||||
<th className='views text-center'>
|
||||
Avg. Video Views
|
||||
</th>
|
||||
<th className='e-commerce text-center'>E-commerce</th>
|
||||
<th className='profile text-center'>Profile</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{session.creators.length <= 0 ? (
|
||||
<tr>
|
||||
<td colSpan='10' className='text-center py-4'>
|
||||
No creators found matching your filters.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
session.creators.map((creator) => (
|
||||
<tr
|
||||
key={creator.id}
|
||||
>
|
||||
<td className='creator-cell'>
|
||||
<Link to={`/creator/${creator.id}`} className='d-flex align-items-center'>
|
||||
<div className='creator-avatar'>
|
||||
<img src={creator.avatar} alt={creator.name} />
|
||||
</div>
|
||||
<div className='creator-name'>
|
||||
{creator.name}
|
||||
</div>
|
||||
</Link>
|
||||
</td>
|
||||
<td className='text-center'>
|
||||
<span className={`category-pill ${getCategoryClassName(creator.category)}`}>
|
||||
{creator.category}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center'>
|
||||
<span className='level-badge ecommerce-level'>{creator.ecommerce_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.has_ecommerce ? <div className='colored-dot bg-primary 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>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -113,7 +113,7 @@ function AddBrandModal({ show, onHide }) {
|
||||
<Form.Select value={brandSource} onChange={(e) => setBrandSource(e.target.value)} required>
|
||||
{BRAND_SOURCES.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
{option.name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
|
@ -75,6 +75,7 @@ export default function CreatorDetail({}) {
|
||||
dispatch(fetchCreatorVideos({ creatorId: id }));
|
||||
};
|
||||
const processChartData = (data, chart) => {
|
||||
if (!data) return;
|
||||
switch (chart) {
|
||||
case 'channel':
|
||||
return {
|
||||
@ -242,17 +243,21 @@ export default function CreatorDetail({}) {
|
||||
<div className='data-charts'>
|
||||
<div className='data-chart'>
|
||||
<div className='chart-title'>GMV per sales channel</div>
|
||||
<Doughnut
|
||||
data={processChartData(selectedCreator?.analytics?.gmv_by_channel, 'channel')}
|
||||
options={{ ...options, cutout: 50 }}
|
||||
/>
|
||||
{selectedCreator?.analytics?.gmv_by_channel && (
|
||||
<Doughnut
|
||||
data={processChartData(selectedCreator?.analytics?.gmv_by_channel, 'channel')}
|
||||
options={{ ...options, cutout: 50 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='data-chart'>
|
||||
<div className='chart-title'>GMV by product category</div>
|
||||
<Doughnut
|
||||
data={processChartData(selectedCreator?.analytics?.gmv_by_category, 'category')}
|
||||
options={{ ...options, cutout: 50 }}
|
||||
/>
|
||||
{selectedCreator?.analytics?.gmv_by_category && (
|
||||
<Doughnut
|
||||
data={processChartData(selectedCreator?.analytics?.gmv_by_category, 'category')}
|
||||
options={{ ...options, cutout: 50 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -525,24 +530,30 @@ function CreatorFollowerInfo({ selectedCreator }) {
|
||||
<div className='followers-data-charts'>
|
||||
<div className='data-chart'>
|
||||
<div className='chart-title'>Follower Gender</div>
|
||||
<Doughnut
|
||||
data={processChartData(selectedCreator?.followerData?.gender, 'gender')}
|
||||
options={{ ...options, cutout: 60 }}
|
||||
/>
|
||||
{selectedCreator?.followerData?.gender && (
|
||||
<Doughnut
|
||||
data={processChartData(selectedCreator?.followerData?.gender, 'gender')}
|
||||
options={{ ...options, cutout: 60 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='data-chart'>
|
||||
<div className='chart-title'>Follower Age</div>
|
||||
<Doughnut
|
||||
data={processChartData(selectedCreator?.followerData?.age, 'age')}
|
||||
options={{ ...options, cutout: 60 }}
|
||||
/>
|
||||
{selectedCreator?.followerData?.age && (
|
||||
<Doughnut
|
||||
data={processChartData(selectedCreator?.followerData?.age, 'age')}
|
||||
options={{ ...options, cutout: 60 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='data-chart'>
|
||||
<div className='chart-title'>Top 5 Locations</div>
|
||||
<Bar
|
||||
data={processChartData(selectedCreator?.followerData?.locations, 'location')}
|
||||
options={barOptions}
|
||||
/>
|
||||
{selectedCreator?.followerData?.locations && (
|
||||
<Bar
|
||||
data={processChartData(selectedCreator?.followerData?.locations, 'location')}
|
||||
options={barOptions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -615,7 +626,9 @@ function CreatorTrends({ selectedCreator }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className='line-chart'>
|
||||
<Line data={processChartData(activeTab)} options={lineOptions} />
|
||||
{selectedCreator?.trendsData && (
|
||||
<Line data={processChartData(activeTab)} options={lineOptions} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -624,9 +637,9 @@ function CreatorVideos({ selectedCreator }) {
|
||||
return (
|
||||
<div className='basic-info-list video-list'>
|
||||
<div className='basic-info-title'>Videos</div>
|
||||
{selectedCreator?.videosData?.regular_videos.total > 0 &&
|
||||
{selectedCreator?.videosData?.regular_videos?.videos?.length > 0 &&
|
||||
selectedCreator?.videosData.regular_videos?.videos.map((video) => (
|
||||
<div className='basic-info-item'>
|
||||
<div key={video.id} className='basic-info-item'>
|
||||
<div className='picture' style={{ backgroundImage: `url(${video.thumbnail_url})` }}></div>
|
||||
<div className='right-side-info'>
|
||||
<Crown size={16} className='crown-icon' />
|
||||
@ -649,9 +662,9 @@ function CreatorVideos({ selectedCreator }) {
|
||||
))}
|
||||
|
||||
<div className='basic-info-title'>Videos with Product</div>
|
||||
{selectedCreator?.videosData?.product_videos?.total > 0 &&
|
||||
{selectedCreator?.videosData?.product_videos?.videos?.length > 0 &&
|
||||
selectedCreator?.videosData?.product_videos?.videos.map((video) => (
|
||||
<div className='basic-info-item'>
|
||||
<div key={video.id} className='basic-info-item'>
|
||||
<div className='picture' style={{ backgroundImage: `url(${video.thumbnail_url})` }}></div>
|
||||
<div className='right-side-info'>
|
||||
<Crown size={16} className='crown-icon' />
|
||||
|
@ -4,7 +4,7 @@ import { Button, Form } from 'react-bootstrap';
|
||||
import '@/styles/CreatorDiscovery.scss';
|
||||
import DiscoveryList from '../components/DiscoveryList';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { fetchDiscovery, fetchDiscoveryByIndividual, fetchDiscoveryByMode } from '../store/slices/discoverySlice';
|
||||
import { fetchDiscovery, fetchDiscoveryByIndividual, fetchDiscoveryByMode, resetStatus } from '../store/slices/discoverySlice';
|
||||
|
||||
export default function CreatorDiscovery() {
|
||||
const [search, setSearch] = useState('');
|
||||
@ -12,7 +12,12 @@ export default function CreatorDiscovery() {
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {}, [dispatch]);
|
||||
useEffect(() => {
|
||||
|
||||
return () => {
|
||||
dispatch(resetStatus());
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
@ -332,7 +332,7 @@ export const fetchCreatorMetrics = createAsyncThunk(
|
||||
try {
|
||||
const response = await api.get(`/daren_detail/creators/${creatorId}/metrics`);
|
||||
if (response.code === 200 || response.code === 201) {
|
||||
return response;
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
@ -348,7 +348,7 @@ export const fetchCreatorFollowers = createAsyncThunk(
|
||||
try {
|
||||
const response = await api.get(`/daren_detail/creator/${creatorId}/followers`);
|
||||
if (response.code === 200) {
|
||||
return response;
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
@ -364,7 +364,7 @@ export const fetchCreatorTrends = createAsyncThunk(
|
||||
try {
|
||||
const response = await api.get(`/daren_detail/creator/${creatorId}/trends`);
|
||||
if (response.code === 200) {
|
||||
return response;
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
@ -380,7 +380,7 @@ export const fetchCreatorVideos = createAsyncThunk(
|
||||
try {
|
||||
const response = await api.get(`/daren_detail/creator/${creatorId}/videos`);
|
||||
if (response.code === 200) {
|
||||
return response;
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
@ -513,16 +513,16 @@ const creatorsSlice = createSlice({
|
||||
state.error = action.error.message;
|
||||
})
|
||||
.addCase(fetchCreatorMetrics.fulfilled, (state, action) => {
|
||||
state.selectedCreator.metricsData = action.payload.data;
|
||||
state.selectedCreator.metricsData = action.payload;
|
||||
})
|
||||
.addCase(fetchCreatorFollowers.fulfilled, (state, action) => {
|
||||
state.selectedCreator.followerData = action.payload.data;
|
||||
state.selectedCreator.followerData = action.payload;
|
||||
})
|
||||
.addCase(fetchCreatorTrends.fulfilled, (state, action) => {
|
||||
state.selectedCreator.trendsData = action.payload.data;
|
||||
state.selectedCreator.trendsData = action.payload;
|
||||
})
|
||||
.addCase(fetchCreatorVideos.fulfilled, (state, action) => {
|
||||
state.selectedCreator.videosData = action.payload.data;
|
||||
state.selectedCreator.videosData = action.payload;
|
||||
})
|
||||
.addCase(searchCreators.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
|
@ -28,7 +28,7 @@ export const fetchDiscovery = createAsyncThunk(
|
||||
'discovery/fetchDiscovery',
|
||||
async (query, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
const response = await api.post('/discovery/creators/search/', {query: query});
|
||||
const response = await api.post('/discovery/creators/search/', { query: query });
|
||||
if (response.code === 200) {
|
||||
return response.data;
|
||||
}
|
||||
@ -72,7 +72,7 @@ export const fetchDiscoveryByIndividual = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
const initialState = {
|
||||
creators: [],
|
||||
sessions: [],
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
@ -80,7 +80,12 @@ const initialState = {
|
||||
const discoverySlice = createSlice({
|
||||
name: 'discovery',
|
||||
initialState,
|
||||
reducers: {},
|
||||
reducers: {
|
||||
resetStatus: (state) => {
|
||||
state.status = 'idle';
|
||||
state.error = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchDiscovery.pending, (state) => {
|
||||
@ -88,7 +93,7 @@ const discoverySlice = createSlice({
|
||||
})
|
||||
.addCase(fetchDiscovery.fulfilled, (state, action) => {
|
||||
state.status = 'succeeded';
|
||||
state.creators = action.payload;
|
||||
state.sessions = action.payload;
|
||||
})
|
||||
.addCase(fetchDiscovery.rejected, (state, action) => {
|
||||
state.status = 'failed';
|
||||
@ -99,7 +104,7 @@ const discoverySlice = createSlice({
|
||||
})
|
||||
.addCase(fetchDiscoveryByMode.fulfilled, (state, action) => {
|
||||
state.status = 'succeeded';
|
||||
state.creators = action.payload;
|
||||
state.sessions = [action.payload];
|
||||
})
|
||||
.addCase(fetchDiscoveryByMode.rejected, (state, action) => {
|
||||
state.status = 'failed';
|
||||
@ -110,7 +115,7 @@ const discoverySlice = createSlice({
|
||||
})
|
||||
.addCase(fetchDiscoveryByIndividual.fulfilled, (state, action) => {
|
||||
state.status = 'succeeded';
|
||||
state.creators = action.payload;
|
||||
state.sessions = [action.payload];
|
||||
})
|
||||
.addCase(fetchDiscoveryByIndividual.rejected, (state, action) => {
|
||||
state.status = 'failed';
|
||||
@ -119,6 +124,6 @@ const discoverySlice = createSlice({
|
||||
},
|
||||
});
|
||||
|
||||
export const {} = discoverySlice.actions;
|
||||
export const { resetStatus } = discoverySlice.actions;
|
||||
|
||||
export default discoverySlice.reducer;
|
||||
|
@ -173,13 +173,14 @@ const chatDateFormat = (date) => {
|
||||
|
||||
export const fetchInboxList = createAsyncThunk('inbox/fetchInboxList', async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await api.get(`/chat-history/`);
|
||||
const response = await api.get('/chat-history/');
|
||||
if (response.code === 200) {
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
throw new Error(response.message);
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.message);
|
||||
return rejectWithValue(error.response.data.message);
|
||||
}
|
||||
});
|
||||
|
||||
@ -247,7 +248,7 @@ const inboxSlice = createSlice({
|
||||
})
|
||||
.addCase(fetchInboxList.rejected, (state, action) => {
|
||||
state.inboxStatus = 'failed';
|
||||
state.error = action.error.message;
|
||||
state.error = action.payload;
|
||||
})
|
||||
.addCase(fetchChatHistory.pending, (state) => {
|
||||
state.chatStatus = 'loading';
|
||||
@ -264,11 +265,11 @@ const inboxSlice = createSlice({
|
||||
})
|
||||
.addCase(fetchChatHistory.rejected, (state, action) => {
|
||||
state.chatStatus = 'failed';
|
||||
state.error = action.error.message;
|
||||
state.error = action.payload;
|
||||
})
|
||||
.addCase(fetchTemplates.rejected, (state, action) => {
|
||||
state.templatesStatus = 'failed';
|
||||
state.error = action.error.message;
|
||||
state.error = action.payload;
|
||||
})
|
||||
.addCase(fetchTemplates.pending, (state) => {
|
||||
state.templatesStatus = 'loading';
|
||||
@ -286,7 +287,7 @@ const inboxSlice = createSlice({
|
||||
})
|
||||
.addCase(editTemplateApi.rejected, (state, action) => {
|
||||
state.templatesStatus = 'failed';
|
||||
state.error = action.error.message;
|
||||
state.error = action.payload;
|
||||
})
|
||||
.addCase(addTemplateApi.pending, (state) => {
|
||||
state.templatesStatus = 'loading';
|
||||
@ -298,7 +299,7 @@ const inboxSlice = createSlice({
|
||||
})
|
||||
.addCase(addTemplateApi.rejected, (state, action) => {
|
||||
state.templatesStatus = 'failed';
|
||||
state.error = action.error.message;
|
||||
state.error = action.payload;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user