[dev]product detail's campaign&creators

This commit is contained in:
susie-laptop 2025-06-06 16:07:16 -04:00
parent 0bd25c4f2a
commit a259dc3403
17 changed files with 416 additions and 244 deletions

View File

@ -1,13 +1,13 @@
import { useEffect, useState } from 'react';
import { Button, Form, Modal } from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { fetchCampaigns } from '../store/slices/brandsSlice';
import { fetchCampaigns } from '../store/slices/campaignSlice';
import SpinningComponent from './SpinningComponent';
import { addCreatorsToCampaign } from '../store/slices/creatorsSlice';
export default function AddToCampaign({ show, onHide }) {
const dispatch = useDispatch();
const { campaigns, status } = useSelector((state) => state.brands);
const { campaigns, status } = useSelector((state) => state.campaign);
const { selectedCreators } = useSelector((state) => state.creators);
const [campaignId, setCampaignId] = useState(null);
const [validated, setValidated] = useState(false);

View File

@ -4,7 +4,7 @@ import { fetchBrands } from '../store/slices/brandsSlice';
import { Card } from 'react-bootstrap';
import { Folders, Hash, Link, Users } from 'lucide-react';
import SpinningComponent from './SpinningComponent';
import { getBrandSourceName } from '../lib/utils';
import { getBrandSourceName } from '../lib/utils.jsx';
export default function BrandsList({ openBrandDetail }) {
const { brands, status, error } = useSelector((state) => state.brands);

View File

@ -2,12 +2,12 @@ import { ChartNoAxesColumnIncreasing, CircleDollarSign, Edit, Eye, Folders, Hash
import { useSelector } from 'react-redux';
export default function CampaignInfo() {
const { selectedCampaign } = useSelector((state) => state.brands);
const { currentCampaign } = useSelector((state) => state.campaign);
return (
<div className='campaign-detail-info shadow-xs'>
<div className='campaign-info-top'>
<div className='campaign-name'>{selectedCampaign.name}</div>
<div className='campaign-descp'>{selectedCampaign.description || '--'}</div>
<div className='campaign-name'>{currentCampaign.name}</div>
<div className='campaign-descp'>{currentCampaign.description || '--'}</div>
<div className='campaign-edit'>
<Edit size={18} />
Edit
@ -19,7 +19,7 @@ export default function CampaignInfo() {
<Layers size={18} />
Service
</div>
<div className='campaign-info-item-value'>{selectedCampaign?.service || '--'}</div>
<div className='campaign-info-item-value'>{currentCampaign?.service || '--'}</div>
</div>
<div className='campaign-info-item'>
<div className='campaign-info-item-label'>
@ -27,9 +27,9 @@ export default function CampaignInfo() {
Category
</div>
<div className='campaign-info-item-value'>
{selectedCampaign?.creator_category || '--'}
{/* {selectedCampaign?.category?.length > 0 &&
selectedCampaign.category.map((cat,index) => <span className='category-tag' key={index}>{cat}</span>)} */}
{currentCampaign?.creator_category || '--'}
{/* {currentCampaign?.category?.length > 0 &&
currentCampaign.category.map((cat,index) => <span className='category-tag' key={index}>{cat}</span>)} */}
</div>
</div>
<div className='campaign-info-item'>
@ -37,28 +37,28 @@ export default function CampaignInfo() {
<UserRoundCheck size={18} />
Followers
</div>
<div className='campaign-info-item-value'>{selectedCampaign?.followers || '--'}</div>
<div className='campaign-info-item-value'>{currentCampaign?.followers || '--'}</div>
</div>
<div className='campaign-info-item'>
<div className='campaign-info-item-label'>
<Tag size={18} />
Creator Category
</div>
<div className='campaign-info-item-value'>{selectedCampaign?.creator_category || '--'}</div>
<div className='campaign-info-item-value'>{currentCampaign?.creator_category || '--'}</div>
</div>
<div className='campaign-info-item'>
<div className='campaign-info-item-label'>
<TrendingUp size={18} />
GMV
</div>
<div className='campaign-info-item-value'>{selectedCampaign?.gmv || '--'}</div>
<div className='campaign-info-item-value'>{currentCampaign?.gmv || '--'}</div>
</div>
<div className='campaign-info-item'>
<div className='campaign-info-item-label'>
<CircleDollarSign size={18} />
Pricing
</div>
<div className='campaign-info-item-value'>{selectedCampaign?.budget || '--'}</div>
<div className='campaign-info-item-value'>{currentCampaign?.budget || '--'}</div>
</div>
<div className='campaign-info-item'>
<div className='campaign-info-item-label'>
@ -66,9 +66,9 @@ export default function CampaignInfo() {
Creator Level
</div>
<div className='campaign-info-item-value'>
{selectedCampaign?.creator_level || '--'}
{/* {selectedCampaign?.creator_level?.length > 0 &&
selectedCampaign.creator_level.map((level,index) => (
{currentCampaign?.creator_level || '--'}
{/* {currentCampaign?.creator_level?.length > 0 &&
currentCampaign.creator_level.map((level,index) => (
<span className='creator-level-tag' key={index}>{level}</span>
))} */}
</div>
@ -78,14 +78,14 @@ export default function CampaignInfo() {
<Eye size={18} />
Views
</div>
<div className='campaign-info-item-value'>{selectedCampaign?.views || '--'}</div>
<div className='campaign-info-item-value'>{currentCampaign?.views || '--'}</div>
</div>
<div className='campaign-info-item'>
<div className='campaign-info-item-label'>
<Hash size={18} />
Creators
</div>
<div className='campaign-info-item-value'>{selectedCampaign?.creators_count || '--'}</div>
<div className='campaign-info-item-value'>{currentCampaign?.creators_count || '--'}</div>
</div>
</div>
</div>

View File

@ -3,7 +3,7 @@ import { useState } from 'react';
import { Button, Modal, Table } from 'react-bootstrap';
import { useSelector, useDispatch } from 'react-redux';
import { Link } from 'react-router-dom';
import { getCategoryClassName } from '../lib/utils';
import { getCategoryClassName } from '../lib/utils.jsx';
export default function DiscoveryList() {
const dispatch = useDispatch();

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchInboxList } from '../store/slices/inboxSlice';
import { fetchChatDetails, selectChat } from '../store/slices/chatSlice';
import { chatDateFormat } from '../lib/utils';
import { chatDateFormat } from '../lib/utils.jsx';
export default function InboxList() {
const dispatch = useDispatch();

View File

@ -1,62 +1,63 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchProductDetail } from '../store/slices/productSlice';
import { fetchCampaignCreators, fetchProductCampaigns, fetchProductDetail } from '../store/slices/productSlice';
import { mockCreators } from '../store/slices/creatorsSlice';
import { Accordion, Table } from 'react-bootstrap';
import { Accordion, Alert, Table } from 'react-bootstrap';
import SpinningComponent from './SpinningComponent';
export default function ProductDetail() {
const selectedProduct = useSelector((state) => state.brands.selectedProduct);
const { currentProduct } = useSelector((state) => state.products);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchProductDetail(selectedProduct.id));
}, [selectedProduct.id]);
dispatch(fetchProductDetail(currentProduct.id));
}, [currentProduct.id]);
return (
<div className='product-details shadow-xs'>
<div className='product-details-header'>
<div className='product-details-header-title'>{selectedProduct.name}</div>
<div className='product-details-header-pid'>PID: {selectedProduct.id}</div>
<div className='product-details-header-title'>{currentProduct.name}</div>
<div className='product-details-header-pid'>PID: {currentProduct.id}</div>
</div>
<div className='product-details-body'>
<div className='product-img'></div>
<div className='product-detail-list'>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.commission_rate}</div>
<div className='product-detail-item-value'>{currentProduct.commission_rate}</div>
<div className='product-detail-item-label'>Commission Rate</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.available_samples}</div>
<div className='product-detail-item-value'>{currentProduct.available_samples}</div>
<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}
{currentProduct.sales_price_max} - {currentProduct.sales_price_min}
</div>
<div className='product-detail-item-label'>Sales Price</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.stock}</div>
<div className='product-detail-item-value'>{currentProduct.stock}</div>
<div className='product-detail-item-label'>Stock</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.items_sold}</div>
<div className='product-detail-item-value'>{currentProduct.items_sold}</div>
<div className='product-detail-item-label'>Items Sold</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.product_rating}</div>
<div className='product-detail-item-value'>{currentProduct.product_rating}</div>
<div className='product-detail-item-label'>Product Rating</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.collab_creators}</div>
<div className='product-detail-item-value'>{currentProduct.collab_creators}</div>
<div className='product-detail-item-label'>Collab Creators</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.gmv}</div>
<div className='product-detail-item-value'>{currentProduct.gmv}</div>
<div className='product-detail-item-label'>GMV Achieved</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.reviews_count}</div>
<div className='product-detail-item-value'>{currentProduct.reviews_count}</div>
<div className='product-detail-item-label'>Views Achieved</div>
</div>
</div>
@ -66,27 +67,26 @@ export default function ProductDetail() {
}
export const CampaignsCollabCreators = () => {
const mockData = [
{
id: 1,
name: 'SUNLINK 拍拍灯',
creatorList: mockCreators,
},
{
id: 2,
name: 'SUNLINK 拍拍灯2',
creatorList: mockCreators,
},
];
const { currentProduct, status } = useSelector((state) => state.products);
const dispatch = useDispatch();
if (mockData.length === 0) {
return <div className='text-center'>No campaigns found</div>;
useEffect(() => {
const fetchCampaigns = async () => {
await dispatch(fetchProductCampaigns(currentProduct.id)).unwrap();
};
fetchCampaigns();
}, []);
if (status === 'loading') {
return <SpinningComponent />;
}
if (!currentProduct.campaigns || currentProduct.campaigns.length < 0) {
return <Alert variant='info'>No campaigns found</Alert>;
}
return (
<Accordion className='campaigns-collab-creators-list' defaultActiveKey={mockData[0].id}>
这个接口是用哪个根据pid获取关联的活动-根据活动获取creatorList
{mockData.map((item) => (
<Accordion className='campaigns-collab-creators-list' defaultActiveKey={currentProduct.campaigns[0].id}>
{currentProduct.campaigns.map((item) => (
<Accordion.Item eventKey={item.id} key={item.id} className='campaigns-collab-creators-item'>
<Accordion.Header>{item.name}</Accordion.Header>
<Accordion.Body>
@ -103,8 +103,8 @@ export const CampaignsCollabCreators = () => {
</tr>
</thead>
<tbody>
{item?.creatorList.length > 0 &&
item?.creatorList.map((creator) => (
{item?.creators?.length > 0 ? (
item?.creators.map((creator) => (
<tr key={creator.id}>
<td>
<div className='white-space-nowrap'>
@ -123,7 +123,14 @@ export const CampaignsCollabCreators = () => {
<td>{creator.pricing || '--'}</td>
<td>{creator.status || '--'}</td>
</tr>
))}
))
) : (
<tr>
<td colSpan={7} className='text-center'>
No creators found
</td>
</tr>
)}
</tbody>
</Table>
</Accordion.Body>

View File

@ -1,4 +1,4 @@
import { Spinner } from "react-bootstrap";
import { Spinner } from 'react-bootstrap';
export default function SpinningComponent() {
return (

View File

@ -1,5 +1,8 @@
import { format, isToday, parseISO } from 'date-fns';
import { BRAND_SOURCES } from "./constant";
import { BRAND_SOURCES } from './constant';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Link } from 'react-router-dom';
import React from 'react';
/**
* 格式化日期
@ -56,11 +59,11 @@ export const getCategoryClassName = (category) => {
};
export const getBrandSourceName = (source) => {
return BRAND_SOURCES.find(item => item.value === source)?.name || source;
return BRAND_SOURCES.find((item) => item.value === source)?.name || source;
};
export const getTemplateMissionName = (mission) => {
return TEMPLATE_MISSIONS.find(item => item.value === mission)?.name || mission;
return TEMPLATE_MISSIONS.find((item) => item.value === mission)?.name || mission;
};
export const chatDateFormat = (date) => {
@ -71,3 +74,23 @@ export const chatDateFormat = (date) => {
return format(now, 'MMM do');
}
};
export function determinProfileIcon(creator) {
if (!creator) return '--';
console.log(creator);
if (creator.profile === 'tiktok') {
return <Link to={creator.tiktok_link || ''} target='_blank'>
<FontAwesomeIcon icon='fa-brands fa-tiktok' />
</Link>;
} else if (creator.profile === 'youtube') {
return <Link to={creator.youtube_link || ''} target='_blank'>
<FontAwesomeIcon icon='fa-brands fa-youtube' />
</Link>;
} else if (creator.profile === 'instagram') {
return <Link to={creator.instagram_link || ''} target='_blank'>
<FontAwesomeIcon icon='fa-brands fa-instagram' />
</Link>;
} else {
return '--';
}
}

View File

@ -10,9 +10,9 @@ import {
fetchBrandDetail,
fetchBrandCampaigns,
fetchBrandProducts,
setSelectedProduct,
createCampaignThunk,
} from '../store/slices/brandsSlice';
import { setCurrentProduct } from '../store/slices/productSlice';
import SlidePanel from '../components/SlidePanel';
import ProductDetail, { CampaignsCollabCreators } from '../components/ProductDetail';
import { CAMPAIGN_SERVICES, CREATOR_CATEGORIES, CREATOR_LEVELS, CREATOR_TYPES, GMV_RANGES } from '../lib/constant';
@ -30,14 +30,18 @@ export default function BrandsDetail() {
useEffect(() => {
if (id) {
dispatch(fetchBrandDetail(id));
const fetchData = async () => {
await dispatch(fetchBrandDetail(id)).unwrap();
dispatch(fetchBrandCampaigns(id));
dispatch(fetchBrandProducts(id));
};
fetchData();
}
}, [dispatch, id]);
const handleShowProductDetail = (product) => {
dispatch(setSelectedProduct(product));
dispatch(setCurrentProduct(product));
setShowProductDetail(true);
};
@ -158,11 +162,7 @@ export default function BrandsDetail() {
onHide={() => setShowAddCampaignModal(false)}
brandId={id}
/>
<AddProductModal
show={showAddProductModal}
onHide={() => setShowAddProductModal(false)}
brandId={id}
/>
<AddProductModal show={showAddProductModal} onHide={() => setShowAddProductModal(false)} brandId={id} />
</React.Fragment>
)
);

View File

@ -3,52 +3,75 @@ import { Link, useParams } from 'react-router-dom';
import SearchBar from '../components/SearchBar';
import { Button, Col, Form, Modal, Row, Spinner, Table } from 'react-bootstrap';
import { useSelector, useDispatch } from 'react-redux';
import { fetchBrands, fetchBrandDetail, fetchCampaignDetail, setSelectedProduct } from '../store/slices/brandsSlice';
import { fetchBrandDetail } from '../store/slices/brandsSlice';
import { fetchCampaignDetail, fetchMatchingResult, resetCurrentCampaign } from '../store/slices/campaignSlice';
import CampaignInfo from '../components/CampaignInfo';
import { ChevronRight, Send, Plus } from 'lucide-react';
import ProductsList from '../components/ProductsList';
import SlidePanel from '../components/SlidePanel';
import CampaignScript from './CampaignScript';
import { addProductToCampaign, fetchProducts } from '../store/slices/productSlice';
import { addProductToCampaign, fetchProducts, setCurrentProduct } from '../store/slices/productSlice';
import { determinProfileIcon } from '../lib/utils.jsx';
export default function CampaignDetail() {
const { brandId, campaignId } = useParams();
const dispatch = useDispatch();
const { brands, selectedBrand, selectedCampaign } = useSelector((state) => state.brands);
const { selectedBrand } = useSelector((state) => state.brands);
const { currentCampaign } = useSelector((state) => state.campaign);
const progressList = ['Find', 'Review', 'Confirmed', 'Draft Ready', 'Published'];
const [progressIndex, setProgressIndex] = useState(2);
const [activeTab, setActiveTab] = useState('products');
const [showProductDetail, setShowProductDetail] = useState(false);
const [showAddProductModal, setShowAddProductModal] = useState(false);
const [additionalRequirements, setAdditionalRequirements] = useState({
criteria: 'default',
top_n: 10,
});
useEffect(() => {
if (brandId && campaignId) {
const fetchData = async () => {
console.log(selectedBrand);
if (!selectedBrand?.id) {
dispatch(fetchBrandDetail(brandId));
await dispatch(fetchBrandDetail(brandId)).unwrap();
dispatch(fetchCampaignDetail(campaignId));
} else {
dispatch(fetchCampaignDetail(campaignId));
}
dispatch(fetchMatchingResult(additionalRequirements));
};
fetchData();
}
dispatch(fetchProducts());
}, [dispatch, brandId, campaignId]);
useEffect(() => {
return () => {
dispatch(resetCurrentCampaign());
};
}, []);
const handleShowProductDetail = (product) => {
dispatch(setSelectedProduct(product));
dispatch(setCurrentProduct(product));
setShowProductDetail(true);
};
const handleMatchCreators = async (e) => {
e.preventDefault();
await dispatch(fetchMatchingResult(additionalRequirements)).unwrap();
};
return (
selectedCampaign?.id && (
currentCampaign && (
<div className='campaign-detail'>
<div className='breadcrumb'>
<Link to={'/brands'} className='breadcrumb-item'>
Brands
</Link>
<Link to={`/brands/${brandId}`} className='breadcrumb-item'>
{selectedBrand.name}
{selectedBrand?.name}
</Link>
<div className='breadcrumb-item'>{selectedCampaign.name}</div>
<div className='breadcrumb-item'>{currentCampaign.name}</div>
</div>
<div className='function-bar'>
<SearchBar />
@ -58,14 +81,27 @@ export default function CampaignDetail() {
</Button>
</div>
<CampaignInfo />
<Form className='campaign-requirements shadow-xs'>
<Form className='campaign-requirements shadow-xs' onSubmit={handleMatchCreators}>
<Form.Group className='mb-3 additional_requirements' controlId='additional_requirements'>
<Form.Label>Additional Requirements</Form.Label>
<Form.Control type='text' placeholder='xxx' />
<Form.Control
type='text'
placeholder=''
value={additionalRequirements.criteria}
onChange={(e) =>
setAdditionalRequirements({ ...additionalRequirements, criteria: e.target.value })
}
/>
</Form.Group>
<Form.Group className='mb-3 creator_requirements' controlId='creator_requirements'>
<Form.Label>Creators</Form.Label>
<Form.Control type='number' />
<Form.Control
type='number'
value={additionalRequirements.top_n}
onChange={(e) =>
setAdditionalRequirements({ ...additionalRequirements, top_n: e.target.value })
}
/>
</Form.Group>
<Button type='submit' variant='primary-subtle'>
Match Creators
@ -135,7 +171,7 @@ export default function CampaignDetail() {
Add Product
</Button>
<ProductsList
products={selectedCampaign?.link_product_details}
products={currentCampaign?.link_product_details}
onShowProductDetail={handleShowProductDetail}
/>
<SlidePanel
@ -216,7 +252,7 @@ function AddProductModal({ campaignId, show, onHide }) {
}
function AcceptedCreators() {
const { selectedCampaign } = useSelector((state) => state.brands);
const { currentCampaign } = useSelector((state) => state.campaign);
const handleSort = (field) => {
return;
@ -263,7 +299,7 @@ function AcceptedCreators() {
<Form.Check
type='checkbox'
checked={
selectedCampaign?.creators?.length === publicCreators.length && publicCreators.length > 0
currentCampaign?.creators?.length === publicCreators.length && publicCreators.length > 0
}
onChange={handleSelectAll}
/>
@ -294,7 +330,7 @@ function AcceptedCreators() {
</tr>
</thead>
<tbody>
{!selectedCampaign?.creators || selectedCampaign?.creators?.length <= 0 ? (
{!currentCampaign?.creators || currentCampaign?.creators?.length <= 0 ? (
<>
<tr>
<td colSpan='9' className='text-center py-4'>
@ -308,11 +344,11 @@ function AcceptedCreators() {
</tr>
</>
) : (
selectedCampaign?.creators?.map((creator) => (
currentCampaign?.creators?.map((creator) => (
<tr
key={creator.creator_id}
className={
selectedCampaign?.creators?.includes(creator.creator_id) ? 'selected' : ''
currentCampaign?.creators?.includes(creator.creator_id) ? 'selected' : ''
}
>
{/* <td>
@ -404,6 +440,7 @@ function StepProgress({ dates = [] }) {
}
function MatchingResult() {
const { currentCampaign } = useSelector((state) => state.campaign);
const [showMatchingResultModal, setShowMatchingResultModal] = useState(false);
const mockData = [
@ -468,18 +505,7 @@ function MatchingResult() {
}
function CampaignMatchingResult({ show, onHide }) {
const mockData = [
{
creator: 'Creator 1',
category: 'Category 1',
followers: 100,
gmv: 100,
avg_video_views: 100,
status: 'Status 1',
pricing: 100,
profile: 'Profile 1',
},
];
const { currentCampaign } = useSelector((state) => state.campaign);
return (
<Modal show={show} onHide={onHide} size='lg'>
@ -503,22 +529,22 @@ function CampaignMatchingResult({ show, onHide }) {
</tr>
</thead>
<tbody>
{mockData.length > 0 ? (
mockData.map((item, index) => (
{currentCampaign?.matching_result?.length > 0 ? (
currentCampaign?.matching_result?.map((item, index) => (
<tr key={index}>
<td>{item.creator}</td>
<td>{item.name}</td>
<td>{item.category}</td>
<td>{item.followers}</td>
<td>{item.gmv}</td>
<td>{item.avg_video_views}</td>
<td>{item.status}</td>
<td>{item.status || '--'}</td>
<td>{item.pricing}</td>
<td>{item.profile}</td>
<td>{determinProfileIcon(item)}</td>
</tr>
))
) : (
<tr>
<td colSpan={6} className='text-center'>
<td colSpan={8} className='text-center'>
No data
</td>
</tr>

View File

@ -7,15 +7,15 @@ import ProductDetail, { CampaignsCollabCreators } from '../components/ProductDet
export default function CampaignScript() {
const dispatch = useDispatch();
const selectedProduct = useSelector((state) => state.brands.selectedProduct);
const { currentProduct } = useSelector((state) => state.products);
const [activeTab, setActiveTab] = useState('collaborationInfo');
useEffect(() => {
console.log(selectedProduct);
}, [selectedProduct]);
console.log(currentProduct);
}, [currentProduct]);
return (
selectedProduct?.id && (
currentProduct?.id && (
<div className='product-script'>
<ProductDetail />
<div className='product-script-switches tab-switches'>

View File

@ -86,7 +86,7 @@ export default function Login() {
/>
</InputGroup>
</Form.Group>
<Button type='submit' loading={isLoading} disabled={!handleValidate()}>
<Button type='submit' disabled={!handleValidate()}>
Sign In
</Button>
</Form>

View File

@ -10,6 +10,7 @@ import discoveryReducer from './slices/discoverySlice';
import notificationBarReducer from './slices/notificationBarSlice';
import productReducer from './slices/productSlice';
import chatReducer from './slices/chatSlice';
import campaignReducer from './slices/campaignSlice';
const authPersistConfig = {
key: 'auth',
@ -26,6 +27,7 @@ const rootReducer = combineReducers({
notificationBar: notificationBarReducer,
products: productReducer,
chat: chatReducer,
campaign: campaignReducer,
});
const store = configureStore({

View File

@ -143,7 +143,9 @@ export const fetchBrandDetail = createAsyncThunk('brands/fetchBrandDetail', asyn
return rejectWithValue(error.message);
}
});
export const fetchBrandCampaigns = createAsyncThunk('brands/fetchBrandCampaigns', async (brandId, { rejectWithValue }) => {
export const fetchBrandCampaigns = createAsyncThunk(
'brands/fetchBrandCampaigns',
async (brandId, { rejectWithValue }) => {
try {
const response = await api.get(`/brands/${brandId}/campaigns/`);
console.log(response);
@ -154,21 +156,12 @@ export const fetchBrandCampaigns = createAsyncThunk('brands/fetchBrandCampaigns'
} catch (error) {
return rejectWithValue(error.message);
}
});
export const fetchCampaignDetail = createAsyncThunk('brands/fetchCampaignDetail', async (campaignId, { rejectWithValue }) => {
try {
const response = await api.get(`/campaigns/${campaignId}/`);
if (response.code === 200) {
return response.data;
}
throw new Error(response.message);
} catch (error) {
return rejectWithValue(error.message);
}
});
);
export const fetchBrandProducts = createAsyncThunk('brands/fetchBrandProducts', async (brandId, { rejectWithValue }) => {
export const fetchBrandProducts = createAsyncThunk(
'brands/fetchBrandProducts',
async (brandId, { rejectWithValue }) => {
try {
const response = await api.get(`/brands/${brandId}/products/`);
if (response.code !== 200) {
@ -178,19 +171,8 @@ export const fetchBrandProducts = createAsyncThunk('brands/fetchBrandProducts',
} catch (error) {
return rejectWithValue(error.message);
}
});
export const fetchCampaigns = createAsyncThunk('brands/fetchCampaigns', async (_, { rejectWithValue }) => {
try {
const response = await api.get('/campaigns/');
if (response.code !== 200) {
throw new Error(response.message);
}
return response.data;
} catch (error) {
return rejectWithValue(error.message);
}
});
);
export const createBrandThunk = createAsyncThunk('brands/createBrand', async (brand, { rejectWithValue, dispatch }) => {
try {
@ -204,7 +186,9 @@ export const createBrandThunk = createAsyncThunk('brands/createBrand', async (br
}
});
export const createCampaignThunk = createAsyncThunk('brands/createCampaign', async (campaign, { rejectWithValue, dispatch }) => {
export const createCampaignThunk = createAsyncThunk(
'brands/createCampaign',
async (campaign, { rejectWithValue, dispatch }) => {
try {
const response = await api.post('/campaigns/', campaign);
console.log(response);
@ -216,16 +200,15 @@ export const createCampaignThunk = createAsyncThunk('brands/createCampaign', asy
dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
return rejectWithValue(error.message);
}
});
}
);
const initialState = {
brands: [],
campaigns: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
selectedBrand: {},
selectedCampaign: {},
selectedProduct: {},
selectedBrand: null,
};
const brandsSlice = createSlice({
@ -238,16 +221,6 @@ const brandsSlice = createSlice({
findBrandById: (state, action) => {
state.selectedBrand = state.brands.find((brand) => brand.id.toString() === action.payload);
},
findCampaignById: (state, action) => {
const { brandId, campaignId } = action.payload;
const brand = state.brands?.find((b) => b.id.toString() === brandId);
state.selectedBrand = brand;
state.selectedCampaign = brand?.campaigns?.find((c) => c.id.toString() === campaignId) || {};
},
setSelectedProduct: (state, action) => {
state.selectedProduct = action.payload;
},
},
extraReducers: (builder) => {
builder
@ -284,17 +257,6 @@ const brandsSlice = createSlice({
state.status = 'failed';
state.error = action.error.message;
})
.addCase(fetchCampaigns.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchCampaigns.fulfilled, (state, action) => {
state.status = 'succeeded';
state.campaigns = action.payload;
})
.addCase(fetchCampaigns.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
})
.addCase(createBrandThunk.pending, (state) => {
state.status = 'loading';
})
@ -316,17 +278,6 @@ const brandsSlice = createSlice({
state.status = 'failed';
state.error = action.error.message;
})
.addCase(fetchCampaignDetail.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchCampaignDetail.fulfilled, (state, action) => {
state.status = 'succeeded';
state.selectedCampaign = action.payload;
})
.addCase(fetchCampaignDetail.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
})
.addCase(createCampaignThunk.pending, (state) => {
state.status = 'loading';
})
@ -351,6 +302,6 @@ const brandsSlice = createSlice({
},
});
export const { selectBrand, findBrandById, findCampaignById, setSelectedProduct } = brandsSlice.actions;
export const { selectBrand, findBrandById, setSelectedProduct } = brandsSlice.actions;
export default brandsSlice.reducer;

View File

@ -0,0 +1,97 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import api from "@/services/api";
export const fetchCampaigns = createAsyncThunk('brands/fetchCampaigns', async (_, { rejectWithValue }) => {
try {
const response = await api.get('/campaigns/');
if (response.code !== 200) {
throw new Error(response.message);
}
return response.data;
} catch (error) {
return rejectWithValue(error.message);
}
});
export const fetchCampaignDetail = createAsyncThunk('brands/fetchCampaignDetail', async (campaignId, { rejectWithValue }) => {
try {
const response = await api.get(`/campaigns/${campaignId}/`);
if (response.code === 200) {
return response.data;
}
throw new Error(response.message);
} catch (error) {
return rejectWithValue(error.message);
}
});
export const fetchMatchingResult = createAsyncThunk('brands/fetchMatchingResult', async (query, { rejectWithValue }) => {
try {
const response = await api.post('/operation/sql_search/', query);
if (response.code !== 200) {
throw new Error(response.message);
}
return response.data;
} catch (error) {
return rejectWithValue(error.message);
}
});
const initialState = {
campaigns: [],
currentCampaign: null,
status: 'idle',
error: null,
};
const campaignSlice = createSlice({
name: 'campaign',
initialState,
reducers: {
setCurrentCampaign: (state, action) => {
state.currentCampaign = action.payload;
},
resetCurrentCampaign: (state) => {
state.currentCampaign = null;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchCampaigns.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchCampaigns.fulfilled, (state, action) => {
state.status = 'succeeded';
state.campaigns = action.payload;
})
.addCase(fetchCampaigns.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
})
.addCase(fetchCampaignDetail.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchCampaignDetail.fulfilled, (state, action) => {
state.status = 'succeeded';
state.currentCampaign = action.payload;
})
.addCase(fetchCampaignDetail.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
})
.addCase(fetchMatchingResult.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchMatchingResult.fulfilled, (state, action) => {
state.status = 'succeeded';
state.currentCampaign.matching_result = action.payload.results;
})
.addCase(fetchMatchingResult.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
});
},
});
export const { setCurrentCampaign, resetCurrentCampaign } = campaignSlice.actions;
export default campaignSlice.reducer;

View File

@ -14,10 +14,12 @@ export const fetchProducts = createAsyncThunk('products/fetchProducts', async (_
}
});
export const addProductToCampaign = createAsyncThunk('products/addProductToCampaign', async (formData, { rejectWithValue, dispatch }) => {
export const addProductToCampaign = createAsyncThunk(
'products/addProductToCampaign',
async (formData, { rejectWithValue, dispatch }) => {
try {
const { campaignId, productId } = formData;
const response = await api.post(`/campaigns/${campaignId}/add_product/`, { product_id:productId });
const response = await api.post(`/campaigns/${campaignId}/add_product/`, { product_id: productId });
if (response.code !== 201 && response.code !== 200) {
throw new Error(response.message);
}
@ -27,9 +29,12 @@ export const addProductToCampaign = createAsyncThunk('products/addProductToCampa
dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
return rejectWithValue(error.message);
}
});
}
);
export const fetchProductDetail = createAsyncThunk('products/fetchProductDetail', async (productId, { rejectWithValue, dispatch }) => {
export const fetchProductDetail = createAsyncThunk(
'products/fetchProductDetail',
async (productId, { rejectWithValue, dispatch }) => {
try {
const response = await api.get(`/products/${productId}/`);
if (response.code !== 200) {
@ -40,54 +45,115 @@ export const fetchProductDetail = createAsyncThunk('products/fetchProductDetail'
dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
return rejectWithValue(error.message);
}
}
);
export const fetchProductCampaigns = createAsyncThunk('products/fetchProductCampaigns', async (productId, { rejectWithValue, dispatch }) => {
try {
const response = await api.get('/campaigns/by-product/', { params: { product_id: productId } });
if (response.code !== 200) {
throw new Error(response.message);
}
if (response.data.campaigns.length > 0) {
response.data.campaigns.forEach((campaign) => {
dispatch(fetchCampaignCreators(campaign.id));
});
}
return response.data;
} catch (error) {
return rejectWithValue(error.message);
}
});
export const fetchCampaignCreators = createAsyncThunk('products/fetchCampaignCreators', async (campaignId, { rejectWithValue }) => {
try {
const response = await api.get(`/campaigns/${campaignId}/creator_list/`);
if (response.code !== 200) {
throw new Error(response.message);
}
return { campaignId, creators: response.data.creators };
} catch (error) {
return rejectWithValue(error.message);
}
});
const initialState = {
products: [],
loading: false,
status: 'idle', // idle, loading, success, error
error: null,
productDetail: null,
currentProduct: null,
};
const productSlice = createSlice({
name: 'products',
initialState,
reducers: {},
reducers: {
setCurrentProduct: (state, action) => {
state.currentProduct = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(fetchProducts.pending, (state) => {
state.loading = true;
state.status = 'loading';
});
builder.addCase(fetchProducts.fulfilled, (state, action) => {
state.products = action.payload;
state.loading = false;
})
state.status = 'success';
});
builder.addCase(fetchProducts.rejected, (state, action) => {
state.error = action.payload;
state.loading = false;
})
state.status = 'error';
});
builder.addCase(addProductToCampaign.pending, (state) => {
state.loading = true;
})
state.status = 'loading';
});
builder.addCase(addProductToCampaign.fulfilled, (state, action) => {
state.products = action.payload;
state.loading = false;
})
state.status = 'success';
});
builder.addCase(addProductToCampaign.rejected, (state, action) => {
state.error = action.payload;
state.loading = false;
})
state.status = 'error';
});
builder.addCase(fetchProductDetail.pending, (state) => {
state.loading = true;
})
state.status = 'loading';
});
builder.addCase(fetchProductDetail.fulfilled, (state, action) => {
state.productDetail = action.payload;
state.loading = false;
})
state.status = 'success';
});
builder.addCase(fetchProductDetail.rejected, (state, action) => {
state.error = action.payload;
state.loading = false;
})
state.status = 'error';
});
builder.addCase(fetchProductCampaigns.pending, (state) => {
state.status = 'loading';
});
builder.addCase(fetchProductCampaigns.fulfilled, (state, action) => {
state.currentProduct.campaigns = action.payload.campaigns || [];
state.status = 'success';
});
builder.addCase(fetchProductCampaigns.rejected, (state, action) => {
state.error = action.payload;
state.status = 'error';
});
builder.addCase(fetchCampaignCreators.pending, (state) => {
state.status = 'loading';
});
builder.addCase(fetchCampaignCreators.fulfilled, (state, action) => {
state.currentProduct.campaigns.find((campaign) => campaign.id === action.payload.campaignId).creators = action.payload.creators;
state.status = 'success';
});
builder.addCase(fetchCampaignCreators.rejected, (state, action) => {
state.error = action.payload;
state.status = 'error';
});
},
});
export const { setCurrentProduct } = productSlice.actions;
export default productSlice.reducer;

View File

@ -197,7 +197,7 @@
.campaigns-list {
display: flex;
gap: 0.875rem;
flex-flow: row wrap;
.campaign-info {
background-color: #fff;
border-radius: 0.375rem;
@ -206,7 +206,7 @@
margin-bottom: 1rem;
border: 2px solid transparent;
transition: 0.25s;
width: 350px;
flex: 1;
display: flex;
flex-flow: column nowrap;
gap: 0.5rem;