[dev]productdetail

This commit is contained in:
susie-laptop 2025-05-26 12:55:28 -04:00
parent 855ea29b92
commit a248d7dedf
8 changed files with 219 additions and 88 deletions

View File

@ -1,7 +1,16 @@
import { useSelector } from "react-redux";
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchProductDetail } from '../store/slices/productSlice';
import { mockCreators } from '../store/slices/creatorsSlice';
import { Accordion, Table } from 'react-bootstrap';
export default function ProductDetail() {
const selectedProduct = useSelector((state) => state.brands.selectedProduct);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchProductDetail(selectedProduct.id));
}, [selectedProduct.id]);
return (
<div className='product-details shadow-xs'>
@ -21,7 +30,9 @@ export default function ProductDetail() {
<div className='product-detail-item-label'>Available Samples</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.sales_price_max} - {selectedProduct.sales_price_min}</div>
<div className='product-detail-item-value'>
{selectedProduct.sales_price_max} - {selectedProduct.sales_price_min}
</div>
<div className='product-detail-item-label'>Sales Price</div>
</div>
<div className='product-detail-item'>
@ -53,3 +64,71 @@ export default function ProductDetail() {
</div>
);
}
export const CampaignsCollabCreators = () => {
const mockData = [
{
id: 1,
name: 'SUNLINK 拍拍灯',
creatorList: mockCreators,
},
{
id: 2,
name: 'SUNLINK 拍拍灯2',
creatorList: mockCreators,
},
];
if (mockData.length === 0) {
return <div className='text-center'>No campaigns found</div>;
}
return (
<Accordion className='campaigns-collab-creators-list' defaultActiveKey={mockData[0].id}>
这个接口是用哪个根据pid获取关联的活动-根据活动获取creatorList
{mockData.map((item) => (
<Accordion.Item eventKey={item.id} key={item.id} className='campaigns-collab-creators-item'>
<Accordion.Header>{item.name}</Accordion.Header>
<Accordion.Body>
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden'>
<thead>
<tr>
<th>Creator</th>
<th>Category</th>
<th>Followers</th>
<th>GMV Generated</th>
<th>Views Generated</th>
<th>Pricing</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{item?.creatorList.length > 0 &&
item?.creatorList.map((creator) => (
<tr key={creator.id}>
<td>
<div className='white-space-nowrap'>
<img
className='creator-avatar'
src={creator.avatar}
alt={creator.name}
/>
<span className='creator-name'>{creator.name}</span>
</div>
</td>
<td>{creator.category}</td>
<td>{creator.followers || '--'}</td>
<td>{creator.gmv || '--'}</td>
<td>{creator.views || '--'}</td>
<td>{creator.pricing || '--'}</td>
<td>{creator.status || '--'}</td>
</tr>
))}
</tbody>
</Table>
</Accordion.Body>
</Accordion.Item>
))}
</Accordion>
);
};

View File

@ -14,7 +14,7 @@ import {
createCampaignThunk,
} from '../store/slices/brandsSlice';
import SlidePanel from '../components/SlidePanel';
import ProductDetail from '../components/ProductDetail';
import ProductDetail, { CampaignsCollabCreators } from '../components/ProductDetail';
import { CAMPAIGN_SERVICES, CREATOR_CATEGORIES, CREATOR_LEVELS, CREATOR_TYPES, GMV_RANGES } from '../lib/constant';
import RangeSlider from '../components/RangeSlider';
import SpinningComponent from '../components/Spinning';
@ -147,7 +147,10 @@ export default function BrandsDetail() {
title='Product Detail'
size='xxl'
>
<ProductDetail />
<div className='product-details-panel'>
<ProductDetail />
<CampaignsCollabCreators />
</div>
</SlidePanel>
</>
)}

View File

@ -1,10 +1,9 @@
import { useDispatch, useSelector } from 'react-redux';
import { useCallback, useEffect, useState } from 'react';
import { Accordion, Button, Card, Col, Form, Row, Table } from 'react-bootstrap';
import { Button, Card, Col, Form, Row } from 'react-bootstrap';
import { CloudUpload, Paperclip } from 'lucide-react';
import { useDropzone } from 'react-dropzone';
import { mockCreators } from '../store/slices/creatorsSlice';
import ProductDetail from '../components/ProductDetail';
import ProductDetail, { CampaignsCollabCreators } from '../components/ProductDetail';
export default function CampaignScript() {
const dispatch = useDispatch();
@ -139,73 +138,6 @@ const CollabInfo = () => {
);
};
const CampaignsCollabCreators = () => {
const mockData = [
{
id: 1,
name: 'SUNLINK 拍拍灯',
creatorList: mockCreators,
},
{
id: 2,
name: 'SUNLINK 拍拍灯2',
creatorList: mockCreators,
},
];
if (mockData.length === 0) {
return <div className='text-center'>No campaigns found</div>;
}
return (
<Accordion className='campaigns-collab-creators-list' defaultActiveKey={mockData[0].id}>
{mockData.map((item) => (
<Accordion.Item eventKey={item.id} key={item.id} className='campaigns-collab-creators-item'>
<Accordion.Header>{item.name}</Accordion.Header>
<Accordion.Body>
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden'>
<thead>
<tr>
<th>Creator</th>
<th>Category</th>
<th>Followers</th>
<th>GMV Generated</th>
<th>Views Generated</th>
<th>Pricing</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{item?.creatorList.length > 0 &&
item?.creatorList.map((creator) => (
<tr key={creator.id}>
<td>
<div className='white-space-nowrap'>
<img
className='creator-avatar'
src={creator.avatar}
alt={creator.name}
/>
<span className='creator-name'>{creator.name}</span>
</div>
</td>
<td>{creator.category}</td>
<td>{creator.followers || '--'}</td>
<td>{creator.gmv || '--'}</td>
<td>{creator.views || '--'}</td>
<td>{creator.pricing || '--'}</td>
<td>{creator.status || '--'}</td>
</tr>
))}
</tbody>
</Table>
</Accordion.Body>
</Accordion.Item>
))}
</Accordion>
);
};
const FileUpload = () => {
const [files, setFiles] = useState([]);

View File

@ -0,0 +1,41 @@
import { createSlice } from '@reduxjs/toolkit';
export const fetchChats = createAsyncThunk('chat/fetchChats', async (_, { rejectWithValue }) => {
try {
const response = await api.get(`/chat-history/search/`);
if (response.code === 200) {
return response.data;
}
throw new Error(response.message);
} catch (error) {
return rejectWithValue(error.message);
}
});
const initialState = {
chats: [],
currentChat: null,
status: 'idle',
error: null,
};
const chatSlice = createSlice({
name: 'chat',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchChats.pending, (state) => {
state.status = 'loading';
})
builder.addCase(fetchChats.fulfilled, (state, action) => {
state.status = 'succeeded';
state.chats = action.payload;
})
builder.addCase(fetchChats.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
});
},
});
export default chatSlice.reducer;

View File

@ -1,4 +1,6 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import api from '@/services/api';
import { setNotificationBarMessage } from './notificationBarSlice';
const mockCreators = [
{
@ -22,11 +24,38 @@ const mockCreators = [
date: '2021-01-01',
},
];
export const fetchDiscovery = createAsyncThunk('discovery/fetchDiscovery', async (search) => {
// const response = await fetch('/api/discovery');
// return response.json();
return mockCreators;
});
export const fetchDiscovery = createAsyncThunk(
'discovery/fetchDiscovery',
async (searchParams, { rejectWithValue }) => {
try {
const response = await api.post('/creators/search/', searchParams);
if (response.code === 200) {
return response.data;
}
throw new Error(response.message);
} catch (error) {
dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
return rejectWithValue(error.message);
}
}
);
export const fetchDiscoveryByMode = createAsyncThunk(
'discovery/fetchDiscoveryByMode',
async (params, { rejectWithValue }) => {
try {
const response = await api.post('/discovery/creators/search_tags/', params);
if (response.code === 200) {
return response.data;
}
throw new Error(response.message);
} catch (error) {
dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
return rejectWithValue(error.message);
}
}
);
const initialState = {
creators: [],
status: 'idle',
@ -49,6 +78,17 @@ const discoverySlice = createSlice({
.addCase(fetchDiscovery.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
})
.addCase(fetchDiscoveryByMode.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchDiscoveryByMode.fulfilled, (state, action) => {
state.status = 'succeeded';
state.creators = action.payload;
})
.addCase(fetchDiscoveryByMode.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
},
});

View File

@ -1,5 +1,7 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { format, isToday, parseISO } from 'date-fns';
import api from '@/services/api';
const mockTemplates = [
{
id: 1,
@ -169,13 +171,16 @@ const chatDateFormat = (date) => {
}
};
export const fetchInboxList = createAsyncThunk('inbox/fetchInboxList', async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
const formattedInboxList = mockInboxList.map((item) => ({
...item,
date: chatDateFormat(item.date),
}));
return formattedInboxList;
export const fetchInboxList = createAsyncThunk('inbox/fetchInboxList', async (_, { rejectWithValue }) => {
try {
const response = await api.get(`/chat-history/`);
if (response.code === 200) {
return response.data;
}
throw new Error(response.message);
} catch (error) {
return rejectWithValue(error.message);
}
});
export const fetchChatHistory = createAsyncThunk('inbox/fetchChatHistory', async (id) => {

View File

@ -29,10 +29,24 @@ export const addProductToCampaign = createAsyncThunk('products/addProductToCampa
}
});
export const fetchProductDetail = createAsyncThunk('products/fetchProductDetail', async (productId, { rejectWithValue, dispatch }) => {
try {
const response = await api.get(`/products/${productId}/`);
if (response.code !== 200) {
throw new Error(response.message);
}
return response.data;
} catch (error) {
dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
return rejectWithValue(error.message);
}
});
const initialState = {
products: [],
loading: false,
error: null,
productDetail: null,
};
const productSlice = createSlice({
@ -62,6 +76,17 @@ const productSlice = createSlice({
state.error = action.payload;
state.loading = false;
})
builder.addCase(fetchProductDetail.pending, (state) => {
state.loading = true;
})
builder.addCase(fetchProductDetail.fulfilled, (state, action) => {
state.productDetail = action.payload;
state.loading = false;
})
builder.addCase(fetchProductDetail.rejected, (state, action) => {
state.error = action.payload;
state.loading = false;
})
},
});

View File

@ -420,3 +420,9 @@
gap: 1rem;
}
}
.product-details-panel {
display: flex;
flex-flow: column nowrap;
gap: 1rem;
}