[dev]discovery render

This commit is contained in:
susie-laptop 2025-05-29 17:56:06 -04:00
parent 6953882a0f
commit 63dfd70117
9 changed files with 197 additions and 70 deletions

View File

@ -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>
);
}

View File

@ -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>
</Link>
</td>
<td>
<td className='text-center'>
<span className={`category-pill ${getCategoryClassName(creator.category)}`}>
{creator.category}
</span>

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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>
{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>
{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>
{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>
{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>
{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'>
{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' />

View File

@ -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();

View File

@ -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';

View File

@ -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;

View File

@ -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);
}
} 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;
});
},
});