[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 ( return (
<div className='brands-list'> <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)}> <div className='brand-card shadow-xs' key={brand.id} onClick={() => openBrandDetail(brand)}>
<Card.Body> <Card.Body>
<Card.Title className='text-primary fw-bold'> <Card.Title className='text-primary fw-bold'>
@ -62,7 +62,9 @@ export default function BrandsList({ openBrandDetail }) {
</Card.Text> </Card.Text>
</Card.Body> </Card.Body>
</div> </div>
))} )) : (
<div className='alert alert-info'>No brands found.</div>
)}
</div> </div>
); );
} }

View File

@ -176,17 +176,17 @@ export default function CreatorList({ path }) {
/> />
</td> </td>
<td className='creator-cell'> <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'> <div className='creator-avatar'>
<img src={creator.avatar} alt={creator.name} /> <img src={creator.avatar} alt={creator.name} />
{creator.status && <span className='verified-badge'></span>} {creator.status && <span className='verified-badge'></span>}
</div> </div>
<Link to={`/creator/${creator.creator_id}`} className='creator-name'> <div className='creator-name'>
{creator.name} {creator.name}
</Link> </div>
</div> </Link>
</td> </td>
<td> <td className='text-center'>
<span className={`category-pill ${getCategoryClassName(creator.category)}`}> <span className={`category-pill ${getCategoryClassName(creator.category)}`}>
{creator.category} {creator.category}
</span> </span>

View File

@ -1,10 +1,11 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Table } from 'react-bootstrap'; import { Table } from 'react-bootstrap';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
export default function DiscoveryList() { export default function DiscoveryList() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { creators, error, status } = useSelector((state) => state.discovery); const { sessions, error, status } = useSelector((state) => state.discovery);
// //
if (status === 'failed') { if (status === 'failed') {
@ -27,18 +28,18 @@ export default function DiscoveryList() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{creators?.length > 0 ? ( {sessions?.length > 0 ? (
creators.map((creator) => ( sessions.map((session) => (
<tr key={creator.id}> <tr key={session.id}>
<td className='text-center'>{creator.sessions}</td> <td className='text-center'>{session.session_number}</td>
<td className='text-center'>{creator.creator}</td> <td className='text-center'>{session.creator_count}</td>
<td className='text-center'>{creator.shoppableCreators}</td> <td className='text-center'>{session.shoppable_creators}</td>
<td className='text-center'>{creator.avgFollowers}</td> <td className='text-center'>{session.avg_followers}</td>
<td className='text-center'>{creator.avgGMV}</td> <td className='text-center'>{session.avg_gmv}</td>
<td className='text-center'>{creator.avgVideoViews}</td> <td className='text-center'>{session.avg_video_views}</td>
<td className='text-center'>{creator.date}</td> <td className='text-center'>{session.date_created}</td>
<td className='text-center'> <td className='text-center'>
<Link to={`/creator/${creator.id}`}>View</Link> <Link to={`/creator/${session.id}`}>View</Link>
</td> </td>
</tr> </tr>
)) ))
@ -54,3 +55,103 @@ export default function DiscoveryList() {
</div> </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> <Form.Select value={brandSource} onChange={(e) => setBrandSource(e.target.value)} required>
{BRAND_SOURCES.map((option) => ( {BRAND_SOURCES.map((option) => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
{option.label} {option.name}
</option> </option>
))} ))}
</Form.Select> </Form.Select>

View File

@ -75,6 +75,7 @@ export default function CreatorDetail({}) {
dispatch(fetchCreatorVideos({ creatorId: id })); dispatch(fetchCreatorVideos({ creatorId: id }));
}; };
const processChartData = (data, chart) => { const processChartData = (data, chart) => {
if (!data) return;
switch (chart) { switch (chart) {
case 'channel': case 'channel':
return { return {
@ -242,17 +243,21 @@ export default function CreatorDetail({}) {
<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 {selectedCreator?.analytics?.gmv_by_channel && (
data={processChartData(selectedCreator?.analytics?.gmv_by_channel, 'channel')} <Doughnut
options={{ ...options, cutout: 50 }} 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 {selectedCreator?.analytics?.gmv_by_category && (
data={processChartData(selectedCreator?.analytics?.gmv_by_category, 'category')} <Doughnut
options={{ ...options, cutout: 50 }} data={processChartData(selectedCreator?.analytics?.gmv_by_category, 'category')}
/> options={{ ...options, cutout: 50 }}
/>
)}
</div> </div>
</div> </div>
</div> </div>
@ -525,24 +530,30 @@ function CreatorFollowerInfo({ selectedCreator }) {
<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 {selectedCreator?.followerData?.gender && (
data={processChartData(selectedCreator?.followerData?.gender, 'gender')} <Doughnut
options={{ ...options, cutout: 60 }} data={processChartData(selectedCreator?.followerData?.gender, 'gender')}
/> 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 {selectedCreator?.followerData?.age && (
data={processChartData(selectedCreator?.followerData?.age, 'age')} <Doughnut
options={{ ...options, cutout: 60 }} data={processChartData(selectedCreator?.followerData?.age, 'age')}
/> 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>
<Bar {selectedCreator?.followerData?.locations && (
data={processChartData(selectedCreator?.followerData?.locations, 'location')} <Bar
options={barOptions} data={processChartData(selectedCreator?.followerData?.locations, 'location')}
/> options={barOptions}
/>
)}
</div> </div>
</div> </div>
</div> </div>
@ -615,7 +626,9 @@ function CreatorTrends({ selectedCreator }) {
</div> </div>
</div> </div>
<div className='line-chart'> <div className='line-chart'>
<Line data={processChartData(activeTab)} options={lineOptions} /> {selectedCreator?.trendsData && (
<Line data={processChartData(activeTab)} options={lineOptions} />
)}
</div> </div>
</div> </div>
); );
@ -624,9 +637,9 @@ function CreatorVideos({ selectedCreator }) {
return ( return (
<div className='basic-info-list video-list'> <div className='basic-info-list video-list'>
<div className='basic-info-title'>Videos</div> <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) => ( 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='picture' style={{ backgroundImage: `url(${video.thumbnail_url})` }}></div>
<div className='right-side-info'> <div className='right-side-info'>
<Crown size={16} className='crown-icon' /> <Crown size={16} className='crown-icon' />
@ -649,9 +662,9 @@ function CreatorVideos({ selectedCreator }) {
))} ))}
<div className='basic-info-title'>Videos with Product</div> <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) => ( 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='picture' style={{ backgroundImage: `url(${video.thumbnail_url})` }}></div>
<div className='right-side-info'> <div className='right-side-info'>
<Crown size={16} className='crown-icon' /> <Crown size={16} className='crown-icon' />

View File

@ -4,7 +4,7 @@ import { Button, Form } from 'react-bootstrap';
import '@/styles/CreatorDiscovery.scss'; import '@/styles/CreatorDiscovery.scss';
import DiscoveryList from '../components/DiscoveryList'; import DiscoveryList from '../components/DiscoveryList';
import { useDispatch } from 'react-redux'; 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() { export default function CreatorDiscovery() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@ -12,7 +12,12 @@ export default function CreatorDiscovery() {
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => {}, [dispatch]); useEffect(() => {
return () => {
dispatch(resetStatus());
};
}, [dispatch]);
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();

View File

@ -332,7 +332,7 @@ export const fetchCreatorMetrics = createAsyncThunk(
try { try {
const response = await api.get(`/daren_detail/creators/${creatorId}/metrics`); const response = await api.get(`/daren_detail/creators/${creatorId}/metrics`);
if (response.code === 200 || response.code === 201) { if (response.code === 200 || response.code === 201) {
return response; return response.data;
} else { } else {
throw new Error(response.message); throw new Error(response.message);
} }
@ -348,7 +348,7 @@ export const fetchCreatorFollowers = createAsyncThunk(
try { try {
const response = await api.get(`/daren_detail/creator/${creatorId}/followers`); const response = await api.get(`/daren_detail/creator/${creatorId}/followers`);
if (response.code === 200) { if (response.code === 200) {
return response; return response.data;
} else { } else {
throw new Error(response.message); throw new Error(response.message);
} }
@ -364,7 +364,7 @@ export const fetchCreatorTrends = createAsyncThunk(
try { try {
const response = await api.get(`/daren_detail/creator/${creatorId}/trends`); const response = await api.get(`/daren_detail/creator/${creatorId}/trends`);
if (response.code === 200) { if (response.code === 200) {
return response; return response.data;
} else { } else {
throw new Error(response.message); throw new Error(response.message);
} }
@ -380,7 +380,7 @@ export const fetchCreatorVideos = createAsyncThunk(
try { try {
const response = await api.get(`/daren_detail/creator/${creatorId}/videos`); const response = await api.get(`/daren_detail/creator/${creatorId}/videos`);
if (response.code === 200) { if (response.code === 200) {
return response; return response.data;
} else { } else {
throw new Error(response.message); throw new Error(response.message);
} }
@ -513,16 +513,16 @@ const creatorsSlice = createSlice({
state.error = action.error.message; state.error = action.error.message;
}) })
.addCase(fetchCreatorMetrics.fulfilled, (state, action) => { .addCase(fetchCreatorMetrics.fulfilled, (state, action) => {
state.selectedCreator.metricsData = action.payload.data; state.selectedCreator.metricsData = action.payload;
}) })
.addCase(fetchCreatorFollowers.fulfilled, (state, action) => { .addCase(fetchCreatorFollowers.fulfilled, (state, action) => {
state.selectedCreator.followerData = action.payload.data; state.selectedCreator.followerData = action.payload;
}) })
.addCase(fetchCreatorTrends.fulfilled, (state, action) => { .addCase(fetchCreatorTrends.fulfilled, (state, action) => {
state.selectedCreator.trendsData = action.payload.data; state.selectedCreator.trendsData = action.payload;
}) })
.addCase(fetchCreatorVideos.fulfilled, (state, action) => { .addCase(fetchCreatorVideos.fulfilled, (state, action) => {
state.selectedCreator.videosData = action.payload.data; state.selectedCreator.videosData = action.payload;
}) })
.addCase(searchCreators.pending, (state) => { .addCase(searchCreators.pending, (state) => {
state.status = 'loading'; state.status = 'loading';

View File

@ -28,7 +28,7 @@ export const fetchDiscovery = createAsyncThunk(
'discovery/fetchDiscovery', 'discovery/fetchDiscovery',
async (query, { rejectWithValue, dispatch }) => { async (query, { rejectWithValue, dispatch }) => {
try { 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) { if (response.code === 200) {
return response.data; return response.data;
} }
@ -72,7 +72,7 @@ export const fetchDiscoveryByIndividual = createAsyncThunk(
} }
); );
const initialState = { const initialState = {
creators: [], sessions: [],
status: 'idle', status: 'idle',
error: null, error: null,
}; };
@ -80,7 +80,12 @@ const initialState = {
const discoverySlice = createSlice({ const discoverySlice = createSlice({
name: 'discovery', name: 'discovery',
initialState, initialState,
reducers: {}, reducers: {
resetStatus: (state) => {
state.status = 'idle';
state.error = null;
},
},
extraReducers: (builder) => { extraReducers: (builder) => {
builder builder
.addCase(fetchDiscovery.pending, (state) => { .addCase(fetchDiscovery.pending, (state) => {
@ -88,7 +93,7 @@ const discoverySlice = createSlice({
}) })
.addCase(fetchDiscovery.fulfilled, (state, action) => { .addCase(fetchDiscovery.fulfilled, (state, action) => {
state.status = 'succeeded'; state.status = 'succeeded';
state.creators = action.payload; state.sessions = action.payload;
}) })
.addCase(fetchDiscovery.rejected, (state, action) => { .addCase(fetchDiscovery.rejected, (state, action) => {
state.status = 'failed'; state.status = 'failed';
@ -99,7 +104,7 @@ const discoverySlice = createSlice({
}) })
.addCase(fetchDiscoveryByMode.fulfilled, (state, action) => { .addCase(fetchDiscoveryByMode.fulfilled, (state, action) => {
state.status = 'succeeded'; state.status = 'succeeded';
state.creators = action.payload; state.sessions = [action.payload];
}) })
.addCase(fetchDiscoveryByMode.rejected, (state, action) => { .addCase(fetchDiscoveryByMode.rejected, (state, action) => {
state.status = 'failed'; state.status = 'failed';
@ -110,7 +115,7 @@ const discoverySlice = createSlice({
}) })
.addCase(fetchDiscoveryByIndividual.fulfilled, (state, action) => { .addCase(fetchDiscoveryByIndividual.fulfilled, (state, action) => {
state.status = 'succeeded'; state.status = 'succeeded';
state.creators = action.payload; state.sessions = [action.payload];
}) })
.addCase(fetchDiscoveryByIndividual.rejected, (state, action) => { .addCase(fetchDiscoveryByIndividual.rejected, (state, action) => {
state.status = 'failed'; state.status = 'failed';
@ -119,6 +124,6 @@ const discoverySlice = createSlice({
}, },
}); });
export const {} = discoverySlice.actions; export const { resetStatus } = discoverySlice.actions;
export default discoverySlice.reducer; export default discoverySlice.reducer;

View File

@ -173,13 +173,14 @@ const chatDateFormat = (date) => {
export const fetchInboxList = createAsyncThunk('inbox/fetchInboxList', async (_, { rejectWithValue }) => { export const fetchInboxList = createAsyncThunk('inbox/fetchInboxList', async (_, { rejectWithValue }) => {
try { try {
const response = await api.get(`/chat-history/`); const response = await api.get('/chat-history/');
if (response.code === 200) { if (response.code === 200) {
return response.data; return response.data;
} else {
throw new Error(response.message);
} }
throw new Error(response.message);
} catch (error) { } catch (error) {
return rejectWithValue(error.message); return rejectWithValue(error.response.data.message);
} }
}); });
@ -247,7 +248,7 @@ const inboxSlice = createSlice({
}) })
.addCase(fetchInboxList.rejected, (state, action) => { .addCase(fetchInboxList.rejected, (state, action) => {
state.inboxStatus = 'failed'; state.inboxStatus = 'failed';
state.error = action.error.message; state.error = action.payload;
}) })
.addCase(fetchChatHistory.pending, (state) => { .addCase(fetchChatHistory.pending, (state) => {
state.chatStatus = 'loading'; state.chatStatus = 'loading';
@ -264,11 +265,11 @@ const inboxSlice = createSlice({
}) })
.addCase(fetchChatHistory.rejected, (state, action) => { .addCase(fetchChatHistory.rejected, (state, action) => {
state.chatStatus = 'failed'; state.chatStatus = 'failed';
state.error = action.error.message; state.error = action.payload;
}) })
.addCase(fetchTemplates.rejected, (state, action) => { .addCase(fetchTemplates.rejected, (state, action) => {
state.templatesStatus = 'failed'; state.templatesStatus = 'failed';
state.error = action.error.message; state.error = action.payload;
}) })
.addCase(fetchTemplates.pending, (state) => { .addCase(fetchTemplates.pending, (state) => {
state.templatesStatus = 'loading'; state.templatesStatus = 'loading';
@ -286,7 +287,7 @@ const inboxSlice = createSlice({
}) })
.addCase(editTemplateApi.rejected, (state, action) => { .addCase(editTemplateApi.rejected, (state, action) => {
state.templatesStatus = 'failed'; state.templatesStatus = 'failed';
state.error = action.error.message; state.error = action.payload;
}) })
.addCase(addTemplateApi.pending, (state) => { .addCase(addTemplateApi.pending, (state) => {
state.templatesStatus = 'loading'; state.templatesStatus = 'loading';
@ -298,7 +299,7 @@ const inboxSlice = createSlice({
}) })
.addCase(addTemplateApi.rejected, (state, action) => { .addCase(addTemplateApi.rejected, (state, action) => {
state.templatesStatus = 'failed'; state.templatesStatus = 'failed';
state.error = action.error.message; state.error = action.payload;
}); });
}, },
}); });