diff --git a/src/components/AddToCampaign.jsx b/src/components/AddToCampaign.jsx index 68841e6..439b884 100644 --- a/src/components/AddToCampaign.jsx +++ b/src/components/AddToCampaign.jsx @@ -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); diff --git a/src/components/BrandsList.jsx b/src/components/BrandsList.jsx index 81636d4..3def728 100644 --- a/src/components/BrandsList.jsx +++ b/src/components/BrandsList.jsx @@ -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); diff --git a/src/components/CampaignInfo.jsx b/src/components/CampaignInfo.jsx index 3394c29..cc19197 100644 --- a/src/components/CampaignInfo.jsx +++ b/src/components/CampaignInfo.jsx @@ -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 (
-
{selectedCampaign.name}
-
{selectedCampaign.description || '--'}
+
{currentCampaign.name}
+
{currentCampaign.description || '--'}
Edit @@ -19,7 +19,7 @@ export default function CampaignInfo() { Service
-
{selectedCampaign?.service || '--'}
+
{currentCampaign?.service || '--'}
@@ -27,9 +27,9 @@ export default function CampaignInfo() { Category
- {selectedCampaign?.creator_category || '--'} - {/* {selectedCampaign?.category?.length > 0 && - selectedCampaign.category.map((cat,index) => {cat})} */} + {currentCampaign?.creator_category || '--'} + {/* {currentCampaign?.category?.length > 0 && + currentCampaign.category.map((cat,index) => {cat})} */}
@@ -37,28 +37,28 @@ export default function CampaignInfo() { Followers
-
{selectedCampaign?.followers || '--'}
+
{currentCampaign?.followers || '--'}
Creator Category
-
{selectedCampaign?.creator_category || '--'}
+
{currentCampaign?.creator_category || '--'}
GMV
-
{selectedCampaign?.gmv || '--'}
+
{currentCampaign?.gmv || '--'}
Pricing
-
{selectedCampaign?.budget || '--'}
+
{currentCampaign?.budget || '--'}
@@ -66,9 +66,9 @@ export default function CampaignInfo() { Creator Level
- {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) => ( {level} ))} */}
@@ -78,14 +78,14 @@ export default function CampaignInfo() { Views
-
{selectedCampaign?.views || '--'}
+
{currentCampaign?.views || '--'}
Creators
-
{selectedCampaign?.creators_count || '--'}
+
{currentCampaign?.creators_count || '--'}
diff --git a/src/components/DiscoveryList.jsx b/src/components/DiscoveryList.jsx index 7d40c0a..761d8a4 100644 --- a/src/components/DiscoveryList.jsx +++ b/src/components/DiscoveryList.jsx @@ -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(); diff --git a/src/components/InboxList.jsx b/src/components/InboxList.jsx index 9e871bd..36edc03 100644 --- a/src/components/InboxList.jsx +++ b/src/components/InboxList.jsx @@ -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(); diff --git a/src/components/ProductDetail.jsx b/src/components/ProductDetail.jsx index c966e94..337a731 100644 --- a/src/components/ProductDetail.jsx +++ b/src/components/ProductDetail.jsx @@ -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 (
-
{selectedProduct.name}
-
PID: {selectedProduct.id}
+
{currentProduct.name}
+
PID: {currentProduct.id}
-
{selectedProduct.commission_rate}
+
{currentProduct.commission_rate}
Commission Rate
-
{selectedProduct.available_samples}
+
{currentProduct.available_samples}
Available Samples
- {selectedProduct.sales_price_max} - {selectedProduct.sales_price_min} + {currentProduct.sales_price_max} - {currentProduct.sales_price_min}
Sales Price
-
{selectedProduct.stock}
+
{currentProduct.stock}
Stock
-
{selectedProduct.items_sold}
+
{currentProduct.items_sold}
Items Sold
-
{selectedProduct.product_rating}
+
{currentProduct.product_rating}
Product Rating
-
{selectedProduct.collab_creators}
+
{currentProduct.collab_creators}
Collab Creators
-
{selectedProduct.gmv}
+
{currentProduct.gmv}
GMV Achieved
-
{selectedProduct.reviews_count}
+
{currentProduct.reviews_count}
Views Achieved
@@ -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
No campaigns found
; + useEffect(() => { + const fetchCampaigns = async () => { + await dispatch(fetchProductCampaigns(currentProduct.id)).unwrap(); + }; + fetchCampaigns(); + }, []); + + if (status === 'loading') { + return ; + } + if (!currentProduct.campaigns || currentProduct.campaigns.length < 0) { + return No campaigns found; } return ( - - 这个接口是用哪个?根据pid获取关联的活动-根据活动获取creatorList - {mockData.map((item) => ( + + {currentProduct.campaigns.map((item) => ( {item.name} @@ -103,8 +103,8 @@ export const CampaignsCollabCreators = () => { - {item?.creatorList.length > 0 && - item?.creatorList.map((creator) => ( + {item?.creators?.length > 0 ? ( + item?.creators.map((creator) => (
@@ -123,7 +123,14 @@ export const CampaignsCollabCreators = () => { {creator.pricing || '--'} {creator.status || '--'} - ))} + )) + ) : ( + + + No creators found + + + )} diff --git a/src/components/SpinningComponent.jsx b/src/components/SpinningComponent.jsx index 76d6f4b..6472736 100644 --- a/src/components/SpinningComponent.jsx +++ b/src/components/SpinningComponent.jsx @@ -1,4 +1,4 @@ -import { Spinner } from "react-bootstrap"; +import { Spinner } from 'react-bootstrap'; export default function SpinningComponent() { return ( diff --git a/src/lib/utils.js b/src/lib/utils.jsx similarity index 65% rename from src/lib/utils.js rename to src/lib/utils.jsx index 5e4589c..018166a 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.jsx @@ -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) => { @@ -70,4 +73,24 @@ export const chatDateFormat = (date) => { } else { return format(now, 'MMM do'); } -}; \ No newline at end of file +}; + +export function determinProfileIcon(creator) { + if (!creator) return '--'; + console.log(creator); + if (creator.profile === 'tiktok') { + return + + ; + } else if (creator.profile === 'youtube') { + return + + ; + } else if (creator.profile === 'instagram') { + return + + ; + } else { + return '--'; + } +} diff --git a/src/pages/BrandsDetail.jsx b/src/pages/BrandsDetail.jsx index 50c7f81..d1ff1ae 100644 --- a/src/pages/BrandsDetail.jsx +++ b/src/pages/BrandsDetail.jsx @@ -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)); - dispatch(fetchBrandCampaigns(id)); - dispatch(fetchBrandProducts(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} /> - setShowAddProductModal(false)} - brandId={id} - /> + setShowAddProductModal(false)} brandId={id} /> ) ); diff --git a/src/pages/CampaignDetail.jsx b/src/pages/CampaignDetail.jsx index bc5b0ca..3013866 100644 --- a/src/pages/CampaignDetail.jsx +++ b/src/pages/CampaignDetail.jsx @@ -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) { - if (!selectedBrand?.id) { - dispatch(fetchBrandDetail(brandId)); - dispatch(fetchCampaignDetail(campaignId)); - } else { - dispatch(fetchCampaignDetail(campaignId)); - } + const fetchData = async () => { + console.log(selectedBrand); + if (!selectedBrand?.id) { + 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 && (
Brands - {selectedBrand.name} + {selectedBrand?.name} -
{selectedCampaign.name}
+
{currentCampaign.name}
@@ -58,14 +81,27 @@ export default function CampaignDetail() {
-
+ Additional Requirements - + + setAdditionalRequirements({ ...additionalRequirements, criteria: e.target.value }) + } + /> Creators - + + setAdditionalRequirements({ ...additionalRequirements, top_n: e.target.value }) + } + /> state.brands); + const { currentCampaign } = useSelector((state) => state.campaign); const handleSort = (field) => { return; @@ -263,7 +299,7 @@ function AcceptedCreators() { 0 + currentCampaign?.creators?.length === publicCreators.length && publicCreators.length > 0 } onChange={handleSelectAll} /> @@ -294,7 +330,7 @@ function AcceptedCreators() { - {!selectedCampaign?.creators || selectedCampaign?.creators?.length <= 0 ? ( + {!currentCampaign?.creators || currentCampaign?.creators?.length <= 0 ? ( <> @@ -308,11 +344,11 @@ function AcceptedCreators() { ) : ( - selectedCampaign?.creators?.map((creator) => ( + currentCampaign?.creators?.map((creator) => ( {/* @@ -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 ( @@ -503,22 +529,22 @@ function CampaignMatchingResult({ show, onHide }) { - {mockData.length > 0 ? ( - mockData.map((item, index) => ( + {currentCampaign?.matching_result?.length > 0 ? ( + currentCampaign?.matching_result?.map((item, index) => ( - {item.creator} + {item.name} {item.category} {item.followers} {item.gmv} {item.avg_video_views} - {item.status} - {item.pricing} - {item.profile} + {item.status || '--'} + {item.pricing} + {determinProfileIcon(item)} )) ) : ( - + No data diff --git a/src/pages/CampaignScript.jsx b/src/pages/CampaignScript.jsx index 527ac87..3ed986b 100644 --- a/src/pages/CampaignScript.jsx +++ b/src/pages/CampaignScript.jsx @@ -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 && (
diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index e55e76b..e2c8908 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -86,7 +86,7 @@ export default function Login() { /> - diff --git a/src/store/index.js b/src/store/index.js index ab047ad..eb58216 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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({ diff --git a/src/store/slices/brandsSlice.js b/src/store/slices/brandsSlice.js index 28f6bae..6229ea4 100644 --- a/src/store/slices/brandsSlice.js +++ b/src/store/slices/brandsSlice.js @@ -143,54 +143,36 @@ export const fetchBrandDetail = createAsyncThunk('brands/fetchBrandDetail', asyn return rejectWithValue(error.message); } }); -export const fetchBrandCampaigns = createAsyncThunk('brands/fetchBrandCampaigns', async (brandId, { rejectWithValue }) => { - try { - const response = await api.get(`/brands/${brandId}/campaigns/`); - console.log(response); - if (response.code === 200) { - return response.data; - } - throw new Error(response.message); - } 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) { +export const fetchBrandCampaigns = createAsyncThunk( + 'brands/fetchBrandCampaigns', + async (brandId, { rejectWithValue }) => { + try { + const response = await api.get(`/brands/${brandId}/campaigns/`); + console.log(response); + if (response.code === 200) { + return response.data; + } 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 }) => { - try { - const response = await api.get('/campaigns/'); - if (response.code !== 200) { - throw new Error(response.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); + } + 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 }) => { try { @@ -204,28 +186,29 @@ export const createBrandThunk = createAsyncThunk('brands/createBrand', async (br } }); -export const createCampaignThunk = createAsyncThunk('brands/createCampaign', async (campaign, { rejectWithValue, dispatch }) => { - try { - const response = await api.post('/campaigns/', campaign); - console.log(response); - if (response.code !== 201 && response.code !== 200) { - throw new Error(response.message); +export const createCampaignThunk = createAsyncThunk( + 'brands/createCampaign', + async (campaign, { rejectWithValue, dispatch }) => { + try { + const response = await api.post('/campaigns/', campaign); + 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 = { 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; diff --git a/src/store/slices/campaignSlice.js b/src/store/slices/campaignSlice.js new file mode 100644 index 0000000..619c37c --- /dev/null +++ b/src/store/slices/campaignSlice.js @@ -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; diff --git a/src/store/slices/productSlice.js b/src/store/slices/productSlice.js index 2b47c2e..7f76a91 100644 --- a/src/store/slices/productSlice.js +++ b/src/store/slices/productSlice.js @@ -14,80 +14,146 @@ export const fetchProducts = createAsyncThunk('products/fetchProducts', async (_ } }); -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 }); - if (response.code !== 201 && response.code !== 200) { - throw new Error(response.message); +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 }); + 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 { - const response = await api.get(`/products/${productId}/`); + 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) { - dispatch(setNotificationBarMessage({ message: error.message, type: '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, + 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; diff --git a/src/styles/Brands.scss b/src/styles/Brands.scss index 7d4a785..bc9c81d 100644 --- a/src/styles/Brands.scss +++ b/src/styles/Brands.scss @@ -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;