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

View File

@ -4,7 +4,7 @@ import { fetchBrands } from '../store/slices/brandsSlice';
import { Card } from 'react-bootstrap'; import { Card } from 'react-bootstrap';
import { Folders, Hash, Link, Users } from 'lucide-react'; import { Folders, Hash, Link, Users } from 'lucide-react';
import SpinningComponent from './SpinningComponent'; import SpinningComponent from './SpinningComponent';
import { getBrandSourceName } from '../lib/utils'; import { getBrandSourceName } from '../lib/utils.jsx';
export default function BrandsList({ openBrandDetail }) { export default function BrandsList({ openBrandDetail }) {
const { brands, status, error } = useSelector((state) => state.brands); 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'; import { useSelector } from 'react-redux';
export default function CampaignInfo() { export default function CampaignInfo() {
const { selectedCampaign } = useSelector((state) => state.brands); const { currentCampaign } = useSelector((state) => state.campaign);
return ( return (
<div className='campaign-detail-info shadow-xs'> <div className='campaign-detail-info shadow-xs'>
<div className='campaign-info-top'> <div className='campaign-info-top'>
<div className='campaign-name'>{selectedCampaign.name}</div> <div className='campaign-name'>{currentCampaign.name}</div>
<div className='campaign-descp'>{selectedCampaign.description || '--'}</div> <div className='campaign-descp'>{currentCampaign.description || '--'}</div>
<div className='campaign-edit'> <div className='campaign-edit'>
<Edit size={18} /> <Edit size={18} />
Edit Edit
@ -19,7 +19,7 @@ export default function CampaignInfo() {
<Layers size={18} /> <Layers size={18} />
Service Service
</div> </div>
<div className='campaign-info-item-value'>{selectedCampaign?.service || '--'}</div> <div className='campaign-info-item-value'>{currentCampaign?.service || '--'}</div>
</div> </div>
<div className='campaign-info-item'> <div className='campaign-info-item'>
<div className='campaign-info-item-label'> <div className='campaign-info-item-label'>
@ -27,9 +27,9 @@ export default function CampaignInfo() {
Category Category
</div> </div>
<div className='campaign-info-item-value'> <div className='campaign-info-item-value'>
{selectedCampaign?.creator_category || '--'} {currentCampaign?.creator_category || '--'}
{/* {selectedCampaign?.category?.length > 0 && {/* {currentCampaign?.category?.length > 0 &&
selectedCampaign.category.map((cat,index) => <span className='category-tag' key={index}>{cat}</span>)} */} currentCampaign.category.map((cat,index) => <span className='category-tag' key={index}>{cat}</span>)} */}
</div> </div>
</div> </div>
<div className='campaign-info-item'> <div className='campaign-info-item'>
@ -37,28 +37,28 @@ export default function CampaignInfo() {
<UserRoundCheck size={18} /> <UserRoundCheck size={18} />
Followers Followers
</div> </div>
<div className='campaign-info-item-value'>{selectedCampaign?.followers || '--'}</div> <div className='campaign-info-item-value'>{currentCampaign?.followers || '--'}</div>
</div> </div>
<div className='campaign-info-item'> <div className='campaign-info-item'>
<div className='campaign-info-item-label'> <div className='campaign-info-item-label'>
<Tag size={18} /> <Tag size={18} />
Creator Category Creator Category
</div> </div>
<div className='campaign-info-item-value'>{selectedCampaign?.creator_category || '--'}</div> <div className='campaign-info-item-value'>{currentCampaign?.creator_category || '--'}</div>
</div> </div>
<div className='campaign-info-item'> <div className='campaign-info-item'>
<div className='campaign-info-item-label'> <div className='campaign-info-item-label'>
<TrendingUp size={18} /> <TrendingUp size={18} />
GMV GMV
</div> </div>
<div className='campaign-info-item-value'>{selectedCampaign?.gmv || '--'}</div> <div className='campaign-info-item-value'>{currentCampaign?.gmv || '--'}</div>
</div> </div>
<div className='campaign-info-item'> <div className='campaign-info-item'>
<div className='campaign-info-item-label'> <div className='campaign-info-item-label'>
<CircleDollarSign size={18} /> <CircleDollarSign size={18} />
Pricing Pricing
</div> </div>
<div className='campaign-info-item-value'>{selectedCampaign?.budget || '--'}</div> <div className='campaign-info-item-value'>{currentCampaign?.budget || '--'}</div>
</div> </div>
<div className='campaign-info-item'> <div className='campaign-info-item'>
<div className='campaign-info-item-label'> <div className='campaign-info-item-label'>
@ -66,9 +66,9 @@ export default function CampaignInfo() {
Creator Level Creator Level
</div> </div>
<div className='campaign-info-item-value'> <div className='campaign-info-item-value'>
{selectedCampaign?.creator_level || '--'} {currentCampaign?.creator_level || '--'}
{/* {selectedCampaign?.creator_level?.length > 0 && {/* {currentCampaign?.creator_level?.length > 0 &&
selectedCampaign.creator_level.map((level,index) => ( currentCampaign.creator_level.map((level,index) => (
<span className='creator-level-tag' key={index}>{level}</span> <span className='creator-level-tag' key={index}>{level}</span>
))} */} ))} */}
</div> </div>
@ -78,14 +78,14 @@ export default function CampaignInfo() {
<Eye size={18} /> <Eye size={18} />
Views Views
</div> </div>
<div className='campaign-info-item-value'>{selectedCampaign?.views || '--'}</div> <div className='campaign-info-item-value'>{currentCampaign?.views || '--'}</div>
</div> </div>
<div className='campaign-info-item'> <div className='campaign-info-item'>
<div className='campaign-info-item-label'> <div className='campaign-info-item-label'>
<Hash size={18} /> <Hash size={18} />
Creators Creators
</div> </div>
<div className='campaign-info-item-value'>{selectedCampaign?.creators_count || '--'}</div> <div className='campaign-info-item-value'>{currentCampaign?.creators_count || '--'}</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@ import { useState } from 'react';
import { Button, Modal, Table } from 'react-bootstrap'; import { Button, Modal, 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';
import { getCategoryClassName } from '../lib/utils'; import { getCategoryClassName } from '../lib/utils.jsx';
export default function DiscoveryList() { export default function DiscoveryList() {
const dispatch = useDispatch(); const dispatch = useDispatch();

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
import { format, isToday, parseISO } from 'date-fns'; 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) => { 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) => { 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) => { export const chatDateFormat = (date) => {
@ -70,4 +73,24 @@ export const chatDateFormat = (date) => {
} else { } else {
return format(now, 'MMM do'); 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, fetchBrandDetail,
fetchBrandCampaigns, fetchBrandCampaigns,
fetchBrandProducts, fetchBrandProducts,
setSelectedProduct,
createCampaignThunk, createCampaignThunk,
} from '../store/slices/brandsSlice'; } from '../store/slices/brandsSlice';
import { setCurrentProduct } from '../store/slices/productSlice';
import SlidePanel from '../components/SlidePanel'; import SlidePanel from '../components/SlidePanel';
import ProductDetail, { CampaignsCollabCreators } from '../components/ProductDetail'; import ProductDetail, { CampaignsCollabCreators } from '../components/ProductDetail';
import { CAMPAIGN_SERVICES, CREATOR_CATEGORIES, CREATOR_LEVELS, CREATOR_TYPES, GMV_RANGES } from '../lib/constant'; import { CAMPAIGN_SERVICES, CREATOR_CATEGORIES, CREATOR_LEVELS, CREATOR_TYPES, GMV_RANGES } from '../lib/constant';
@ -30,14 +30,18 @@ export default function BrandsDetail() {
useEffect(() => { useEffect(() => {
if (id) { if (id) {
dispatch(fetchBrandDetail(id)); const fetchData = async () => {
dispatch(fetchBrandCampaigns(id)); await dispatch(fetchBrandDetail(id)).unwrap();
dispatch(fetchBrandProducts(id)); dispatch(fetchBrandCampaigns(id));
dispatch(fetchBrandProducts(id));
};
fetchData();
} }
}, [dispatch, id]); }, [dispatch, id]);
const handleShowProductDetail = (product) => { const handleShowProductDetail = (product) => {
dispatch(setSelectedProduct(product)); dispatch(setCurrentProduct(product));
setShowProductDetail(true); setShowProductDetail(true);
}; };
@ -158,11 +162,7 @@ export default function BrandsDetail() {
onHide={() => setShowAddCampaignModal(false)} onHide={() => setShowAddCampaignModal(false)}
brandId={id} brandId={id}
/> />
<AddProductModal <AddProductModal show={showAddProductModal} onHide={() => setShowAddProductModal(false)} brandId={id} />
show={showAddProductModal}
onHide={() => setShowAddProductModal(false)}
brandId={id}
/>
</React.Fragment> </React.Fragment>
) )
); );

View File

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

View File

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

View File

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

View File

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

View File

@ -143,54 +143,36 @@ export const fetchBrandDetail = createAsyncThunk('brands/fetchBrandDetail', asyn
return rejectWithValue(error.message); return rejectWithValue(error.message);
} }
}); });
export const fetchBrandCampaigns = createAsyncThunk('brands/fetchBrandCampaigns', async (brandId, { rejectWithValue }) => { export const fetchBrandCampaigns = createAsyncThunk(
try { 'brands/fetchBrandCampaigns',
const response = await api.get(`/brands/${brandId}/campaigns/`); async (brandId, { rejectWithValue }) => {
console.log(response); try {
if (response.code === 200) { const response = await api.get(`/brands/${brandId}/campaigns/`);
return response.data; console.log(response);
} 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 fetchBrandProducts = createAsyncThunk('brands/fetchBrandProducts', async (brandId, { rejectWithValue }) => {
try {
const response = await api.get(`/brands/${brandId}/products/`);
if (response.code !== 200) {
throw new Error(response.message); throw new Error(response.message);
} catch (error) {
return rejectWithValue(error.message);
} }
return response.data;
} catch (error) {
return rejectWithValue(error.message);
} }
}); );
export const fetchCampaigns = createAsyncThunk('brands/fetchCampaigns', async (_, { rejectWithValue }) => { export const fetchBrandProducts = createAsyncThunk(
try { 'brands/fetchBrandProducts',
const response = await api.get('/campaigns/'); async (brandId, { rejectWithValue }) => {
if (response.code !== 200) { try {
throw new Error(response.message); const response = await api.get(`/brands/${brandId}/products/`);
if (response.code !== 200) {
throw new Error(response.message);
}
return response.data;
} catch (error) {
return rejectWithValue(error.message);
} }
return response.data;
} catch (error) {
return rejectWithValue(error.message);
} }
}); );
export const createBrandThunk = createAsyncThunk('brands/createBrand', async (brand, { rejectWithValue, dispatch }) => { export const createBrandThunk = createAsyncThunk('brands/createBrand', async (brand, { rejectWithValue, dispatch }) => {
try { try {
@ -204,28 +186,29 @@ export const createBrandThunk = createAsyncThunk('brands/createBrand', async (br
} }
}); });
export const createCampaignThunk = createAsyncThunk('brands/createCampaign', async (campaign, { rejectWithValue, dispatch }) => { export const createCampaignThunk = createAsyncThunk(
try { 'brands/createCampaign',
const response = await api.post('/campaigns/', campaign); async (campaign, { rejectWithValue, dispatch }) => {
console.log(response); try {
if (response.code !== 201 && response.code !== 200) { const response = await api.post('/campaigns/', campaign);
throw new Error(response.message); console.log(response);
if (response.code !== 201 && response.code !== 200) {
throw new Error(response.message);
}
} catch (error) {
console.log(error);
dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
return rejectWithValue(error.message);
} }
} catch (error) {
console.log(error);
dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
return rejectWithValue(error.message);
} }
}); );
const initialState = { const initialState = {
brands: [], brands: [],
campaigns: [], campaigns: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null, error: null,
selectedBrand: {}, selectedBrand: null,
selectedCampaign: {},
selectedProduct: {},
}; };
const brandsSlice = createSlice({ const brandsSlice = createSlice({
@ -238,16 +221,6 @@ const brandsSlice = createSlice({
findBrandById: (state, action) => { findBrandById: (state, action) => {
state.selectedBrand = state.brands.find((brand) => brand.id.toString() === action.payload); 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) => { extraReducers: (builder) => {
builder builder
@ -284,17 +257,6 @@ const brandsSlice = createSlice({
state.status = 'failed'; state.status = 'failed';
state.error = action.error.message; 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) => { .addCase(createBrandThunk.pending, (state) => {
state.status = 'loading'; state.status = 'loading';
}) })
@ -316,17 +278,6 @@ const brandsSlice = createSlice({
state.status = 'failed'; state.status = 'failed';
state.error = action.error.message; 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) => { .addCase(createCampaignThunk.pending, (state) => {
state.status = 'loading'; 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; 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,80 +14,146 @@ export const fetchProducts = createAsyncThunk('products/fetchProducts', async (_
} }
}); });
export const addProductToCampaign = createAsyncThunk('products/addProductToCampaign', async (formData, { rejectWithValue, dispatch }) => { export const addProductToCampaign = createAsyncThunk(
try { 'products/addProductToCampaign',
const { campaignId, productId } = formData; async (formData, { rejectWithValue, dispatch }) => {
const response = await api.post(`/campaigns/${campaignId}/add_product/`, { product_id:productId }); try {
if (response.code !== 201 && response.code !== 200) { const { campaignId, productId } = formData;
throw new Error(response.message); const response = await api.post(`/campaigns/${campaignId}/add_product/`, { product_id: productId });
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);
} }
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 }) => { 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);
}
}
);
export const fetchProductCampaigns = createAsyncThunk('products/fetchProductCampaigns', async (productId, { rejectWithValue, dispatch }) => {
try { try {
const response = await api.get(`/products/${productId}/`); const response = await api.get('/campaigns/by-product/', { params: { product_id: productId } });
if (response.code !== 200) { if (response.code !== 200) {
throw new Error(response.message); throw new Error(response.message);
} }
if (response.data.campaigns.length > 0) {
response.data.campaigns.forEach((campaign) => {
dispatch(fetchCampaignCreators(campaign.id));
});
}
return response.data; return response.data;
} catch (error) { } catch (error) {
dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
return rejectWithValue(error.message); 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 = { const initialState = {
products: [], products: [],
loading: false, status: 'idle', // idle, loading, success, error
error: null, error: null,
productDetail: null, productDetail: null,
currentProduct: null,
}; };
const productSlice = createSlice({ const productSlice = createSlice({
name: 'products', name: 'products',
initialState, initialState,
reducers: {}, reducers: {
setCurrentProduct: (state, action) => {
state.currentProduct = action.payload;
},
},
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(fetchProducts.pending, (state) => { builder.addCase(fetchProducts.pending, (state) => {
state.loading = true; state.status = 'loading';
}); });
builder.addCase(fetchProducts.fulfilled, (state, action) => { builder.addCase(fetchProducts.fulfilled, (state, action) => {
state.products = action.payload; state.products = action.payload;
state.loading = false; state.status = 'success';
}) });
builder.addCase(fetchProducts.rejected, (state, action) => { builder.addCase(fetchProducts.rejected, (state, action) => {
state.error = action.payload; state.error = action.payload;
state.loading = false; state.status = 'error';
}) });
builder.addCase(addProductToCampaign.pending, (state) => { builder.addCase(addProductToCampaign.pending, (state) => {
state.loading = true; state.status = 'loading';
}) });
builder.addCase(addProductToCampaign.fulfilled, (state, action) => { builder.addCase(addProductToCampaign.fulfilled, (state, action) => {
state.products = action.payload; state.products = action.payload;
state.loading = false; state.status = 'success';
}) });
builder.addCase(addProductToCampaign.rejected, (state, action) => { builder.addCase(addProductToCampaign.rejected, (state, action) => {
state.error = action.payload; state.error = action.payload;
state.loading = false; state.status = 'error';
}) });
builder.addCase(fetchProductDetail.pending, (state) => { builder.addCase(fetchProductDetail.pending, (state) => {
state.loading = true; state.status = 'loading';
}) });
builder.addCase(fetchProductDetail.fulfilled, (state, action) => { builder.addCase(fetchProductDetail.fulfilled, (state, action) => {
state.productDetail = action.payload; state.productDetail = action.payload;
state.loading = false; state.status = 'success';
}) });
builder.addCase(fetchProductDetail.rejected, (state, action) => { builder.addCase(fetchProductDetail.rejected, (state, action) => {
state.error = action.payload; 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; export default productSlice.reducer;

View File

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