Compare commits

..

No commits in common. "a248d7dedff3d57fe11d9dee83b712a4d053c1b7" and "ce1b944103b1be5aa2189f7576abd98f8bf8892f" have entirely different histories.

11 changed files with 107 additions and 426 deletions

View File

@ -1,17 +1,8 @@
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';
import { useSelector } from "react-redux";
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'>
<div className='product-details-header'>
@ -30,9 +21,7 @@ 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'>
@ -64,71 +53,3 @@ 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

@ -1,20 +1,15 @@
import React, { useEffect, useState } from 'react';
import { Table, Form, Button, Modal } from 'react-bootstrap';
import { Table, Form } from 'react-bootstrap';
import { useSelector, useDispatch } from 'react-redux';
import '../styles/Products.scss';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Spinning from './Spinning';
import { Plus } from 'lucide-react';
import SpinningComponent from './Spinning';
import { addProductToCampaign } from '../store/slices/productSlice';
export default function ProductsList({ products, onShowProductDetail, type = 'default' }) {
const { selectedBrand, status } = useSelector((state) => state.brands);
export default function ProductsList({ products, setSelectedProduct, onShowProductDetail }) {
const { selectedBrand } = useSelector((state) => state.brands);
const [sortField, setSortField] = useState(null);
const [sortDirection, setSortDirection] = useState('asc');
const [selectedProducts, setSelectedProducts] = useState([]);
const [showAddProductToCampaignModal, setShowAddProductToCampaignModal] = useState(false);
const [selectedProduct, setSelectedProduct] = useState(null);
const handleSort = (field) => {
return;
@ -49,9 +44,10 @@ export default function ProductsList({ products, onShowProductDetail, type = 'de
};
if (status === 'loading') {
return <SpinningComponent />;
return <Spinning />;
}
return (
<div className='products-list rounded shadow-xs'>
<Table responsive hover className='bg-white rounded overflow-hidden m-0'>
@ -91,7 +87,6 @@ export default function ProductsList({ products, onShowProductDetail, type = 'de
<th className='tiktokShop text-center' onClick={() => handleSort('tiktokShop')}>
TikTok Shop {renderSortIcon('tiktokShop')}
</th>
{type === 'brand' && <th className='actions text-center'>Add To Campaign</th>}
</tr>
</thead>
<tbody>
@ -112,24 +107,18 @@ export default function ProductsList({ products, onShowProductDetail, type = 'de
/>
</td>
<td className='product-cell'>
<div
className='d-flex align-items-center'
onClick={() => onShowProductDetail(product)}
style={{ cursor: 'pointer' }}
>
<div className='d-flex align-items-center' onClick={() => onShowProductDetail(product)} style={{cursor: 'pointer'}}>
<img className='product-logo' src={product.image_url} alt={product.name} />
{/* <div className='product-logo'>{product.name.slice(0, 1)}</div> */}
<div className='product-name'>{product.name}</div>
</div>
</td>
<td className='text-center'>
<td className='text-center' >
<div>{product.commission_rate}</div>
<div className='small text-muted'>Open collab. {product.open_collab}</div>
</td>
<td className='text-center'>{product.available_samples}</td>
<td className='text-center'>
{product.sales_price_min} - {product.sales_price_max}
</td>
<td className='text-center'>{product.sales_price_min} - {product.sales_price_max}</td>
<td className='text-center'>{product.stock}</td>
<td className='text-center'>{product.items_sold}</td>
<td className='text-center'>
@ -137,74 +126,12 @@ export default function ProductsList({ products, onShowProductDetail, type = 'de
<div className='small text-muted'>{product.reviews_count} Reviews</div>
</td>
<td className='text-center'>{product.collab_creators}</td>
<td className='text-center'>
{product.tiktok_shop && <FontAwesomeIcon icon='fa-brands fa-tiktok' />}
</td>
{type === 'brand' && (
<td className='text-center'>
<button className='border-0 bg-transparent text-primary' onClick={() => {
setSelectedProduct(product.id);
setShowAddProductToCampaignModal(true);
}}>
<Plus />
</button>
</td>
)}
<td className='text-center'>{product.tiktok_shop && <FontAwesomeIcon icon='fa-brands fa-tiktok' />}</td>
</tr>
))
)}
</tbody>
</Table>
<AddProductToCampaignModal show={showAddProductToCampaignModal} onHide={() => setShowAddProductToCampaignModal(false)} productId={selectedProduct} />
</div>
);
}
function AddProductToCampaignModal({ show, onHide, productId }) {
const [campaignId, setCampaignId] = useState(null);
const { selectedBrand } = useSelector((state) => state.brands);
const dispatch = useDispatch();
const handleSubmit = async (e) => {
e.preventDefault();
console.log(campaignId, productId);
await dispatch(addProductToCampaign({ campaignId, productId })).unwrap();
handleCancel();
};
const handleCancel = () => {
setCampaignId(null);
onHide();
};
return (
<Modal show={show} onHide={onHide} size='md'>
<Modal.Header closeButton className='fw-bold'>
Add Product
</Modal.Header>
<Form onSubmit={handleSubmit}>
<Modal.Body>
<Form.Group>
<Form.Label>Campaign Name</Form.Label>
<Form.Select required value={campaignId} onChange={(e) => setCampaignId(e.target.value)}>
<option value=''>Select Campaign</option>
{selectedBrand?.campaigns?.map((campaign) => (
<option key={campaign.id} value={campaign.id}>
{campaign.name}
</option>
))}
</Form.Select>
</Form.Group>
</Modal.Body>
<Modal.Footer>
<Button variant='outline-primary' className='border-0' onClick={handleCancel}>
Cancel
</Button>
<Button variant='primary' type='submit'>
Add
</Button>
</Modal.Footer>
</Form>
</Modal>
);
}

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import SearchBar from '../components/SearchBar';
import { Alert, Button, Form, Modal } from 'react-bootstrap';
import { Folders, Hash, LinkIcon, Plus, Users, X } from 'lucide-react';
import { Folders, Hash, LinkIcon, Plus, Users } from 'lucide-react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useParams } from 'react-router-dom';
import CampaignList from '../components/CampaignList';
@ -14,11 +14,10 @@ import {
createCampaignThunk,
} from '../store/slices/brandsSlice';
import SlidePanel from '../components/SlidePanel';
import ProductDetail, { CampaignsCollabCreators } from '../components/ProductDetail';
import ProductDetail 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';
import { setNotificationBarMessage } from '../store/slices/notificationBarSlice';
export default function BrandsDetail() {
const { id } = useParams();
@ -27,7 +26,6 @@ export default function BrandsDetail() {
const { selectedBrand } = useSelector((state) => state.brands);
const [showProductDetail, setShowProductDetail] = useState(false);
const [showAddCampaignModal, setShowAddCampaignModal] = useState(false);
const [showAddProductModal, setShowAddProductModal] = useState(false);
useEffect(() => {
if (id) {
@ -54,7 +52,7 @@ export default function BrandsDetail() {
</Button>
)}
{activeTab === 'products' && (
<Button onClick={() => setShowAddProductModal(true)}>
<Button>
<Plus />
Add Product
</Button>
@ -139,7 +137,6 @@ export default function BrandsDetail() {
<ProductsList
products={selectedBrand?.products}
onShowProductDetail={handleShowProductDetail}
type='brand'
/>
<SlidePanel
show={showProductDetail}
@ -147,15 +144,11 @@ export default function BrandsDetail() {
title='Product Detail'
size='xxl'
>
<div className='product-details-panel'>
<ProductDetail />
<CampaignsCollabCreators />
</div>
<ProductDetail />
</SlidePanel>
</>
)}
<AddCampaignModal show={showAddCampaignModal} onHide={() => setShowAddCampaignModal(false)} />
<AddProductModal show={showAddProductModal} onHide={() => setShowAddProductModal(false)} />
</React.Fragment>
)
);
@ -225,7 +218,7 @@ export function AddCampaignModal({ show, onHide }) {
};
return (
<Modal show={show} onHide={handleClose}>
<Modal show={show} onHide={handleClose} >
<Modal.Header>
<Modal.Title>Add Campaign</Modal.Title>
</Modal.Header>
@ -375,111 +368,3 @@ export function AddCampaignModal({ show, onHide }) {
</Modal>
);
}
export function AddProductModal({ show, onHide }) {
const dispatch = useDispatch();
const [productIds, setProductIds] = useState({
0: '',
});
const addProduct = () => {
const newIndex = Object.keys(productIds).length;
console.log(newIndex);
setProductIds((prev) => ({
...prev,
[newIndex]: '',
}));
};
const handleChange = (e) => {
const { name, value } = e.target;
setProductIds((prev) => ({
...prev,
[name]: value,
}));
};
const handleClose = () => {
setProductIds([]);
onHide();
};
const handleSubmit = (e) => {
e.preventDefault();
dispatch(
setNotificationBarMessage({
message: '这个接口是哪一个?',
variant: 'info',
})
);
const form = document.getElementById('addProductForm');
if (form.checkValidity() === false) {
e.preventDefault();
e.stopPropagation();
setValidated(true);
return;
}
console.log(productIds);
};
const removeProduct = (index) => {
if (Object.keys(productIds).length === 1) {
return;
}
setProductIds((prev) => {
const updated = { ...prev };
delete updated[index];
return updated;
});
};
useEffect(() => {
if (show) {
setProductIds({
0: '',
});
}
}, [show]);
return (
<Modal show={show} onHide={handleClose}>
<Modal.Header>
<Modal.Title>Add Product</Modal.Title>
</Modal.Header>
<Form id='addProductForm' onSubmit={handleSubmit}>
<Modal.Body>
{Object.entries(productIds).map(([index, productId]) => (
<Form.Group key={Number(index)}>
<Form.Label>Product {Number(index) + 1} PID</Form.Label>
<div className='d-flex flex-row align-items-center justify-content-between'>
<Form.Control
type='text'
name={index}
value={productId}
onChange={handleChange}
placeholder='Enter Product ID'
required
/>
<Button variant='link' className='border-0' onClick={() => removeProduct(index)}>
<X />
</Button>
</div>
</Form.Group>
))}
<Button variant='outline-primary' className='mt-3' onClick={addProduct}>
Add Product
</Button>
</Modal.Body>
<Modal.Footer>
<Button variant='outline-primary' className='border-0' onClick={onHide}>
Cancel
</Button>
<Button variant='primary' type='submit'>
Submit
</Button>
</Modal.Footer>
</Form>
</Modal>
);
}

View File

@ -170,7 +170,7 @@ function AddProductModal({ campaignId, show, onHide }) {
};
return (
<Modal show={show} onHide={handleCancel} size='sm'>
<Modal show={show} onHide={handleCancel}>
<Modal.Header closeButton className='fw-bold'>
Add Product
</Modal.Header>

View File

@ -1,9 +1,10 @@
import { useDispatch, useSelector } from 'react-redux';
import { useCallback, useEffect, useState } from 'react';
import { Button, Card, Col, Form, Row } from 'react-bootstrap';
import { Accordion, Button, Card, Col, Form, Row, Table } from 'react-bootstrap';
import { CloudUpload, Paperclip } from 'lucide-react';
import { useDropzone } from 'react-dropzone';
import ProductDetail, { CampaignsCollabCreators } from '../components/ProductDetail';
import { mockCreators } from '../store/slices/creatorsSlice';
import ProductDetail from '../components/ProductDetail';
export default function CampaignScript() {
const dispatch = useDispatch();
@ -138,6 +139,73 @@ 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

@ -1,41 +0,0 @@
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,6 +1,4 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import api from '@/services/api';
import { setNotificationBarMessage } from './notificationBarSlice';
const mockCreators = [
{
@ -24,38 +22,11 @@ const mockCreators = [
date: '2021-01-01',
},
];
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);
}
}
);
export const fetchDiscovery = createAsyncThunk('discovery/fetchDiscovery', async (search) => {
// const response = await fetch('/api/discovery');
// return response.json();
return mockCreators;
});
const initialState = {
creators: [],
status: 'idle',
@ -78,17 +49,6 @@ 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,7 +1,5 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { format, isToday, parseISO } from 'date-fns';
import api from '@/services/api';
const mockTemplates = [
{
id: 1,
@ -171,16 +169,13 @@ const chatDateFormat = (date) => {
}
};
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 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 fetchChatHistory = createAsyncThunk('inbox/fetchChatHistory', async (id) => {

View File

@ -21,20 +21,8 @@ export const addProductToCampaign = createAsyncThunk('products/addProductToCampa
if (response.code !== 201 && response.code !== 200) {
throw new Error(response.message);
}
dispatch(setNotificationBarMessage({ message: response.message, type: 'success' }));
return response.data;
} catch (error) {
dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
return rejectWithValue(error.message);
}
});
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);
}
console.log(response);
return response.data;
} catch (error) {
dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
@ -46,7 +34,6 @@ const initialState = {
products: [],
loading: false,
error: null,
productDetail: null,
};
const productSlice = createSlice({
@ -76,17 +63,6 @@ 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

@ -411,18 +411,3 @@
}
}
}
#addProductForm {
.modal-body {
width: 400px;
display: flex;
flex-flow: column nowrap;
gap: 1rem;
}
}
.product-details-panel {
display: flex;
flex-flow: column nowrap;
gap: 1rem;
}

View File

@ -134,3 +134,8 @@ a {
box-shadow: 0px 0px 1px #171a1f12, 0px 0px 2px #171a1f1f; /* shadow-xs */
padding: 1rem;
}
.modal-content {
width: max-content;
max-width: 60vw;
}