onShowProductDetail(product)} style={{cursor: 'pointer'}}>
- {product.name.slice(0, 1)}
+ 
+ {/* {product.name.slice(0, 1)} */}
{product.name}
|
diff --git a/src/components/RangeSlider.jsx b/src/components/RangeSlider.jsx
index b1fbdf8..0657d8f 100644
--- a/src/components/RangeSlider.jsx
+++ b/src/components/RangeSlider.jsx
@@ -2,9 +2,9 @@ import React, { useState, useEffect, useRef } from 'react';
import '../styles/RangeSlider.scss';
import debounce from 'lodash/debounce';
-export default function RangeSlider({ min = 0, max = 100, value, onChange }) {
+export default function RangeSlider({ value, onChange, discreteValues }) {
// 预定义的离散值点
- const discreteValues = [0, 100, 1000, 10000, 100000, 250000, 500000];
+ // const discreteValues = [0, 100, 1000, 10000, 100000, 250000, 500000];
const marks = ['0', '100', '1k', '10k', '100k', '250k', '500k+'];
// 使用索引位置作为滑块的实际值,以实现等分
@@ -170,8 +170,8 @@ export default function RangeSlider({ min = 0, max = 100, value, onChange }) {
- {marks.map((mark, index) => (
- {mark}
+ {discreteValues.map((mark, index) => (
+ {formatValue(mark)}
))}
{/* 显示当前选中的值 */}
diff --git a/src/lib/constant.js b/src/lib/constant.js
new file mode 100644
index 0000000..55ec385
--- /dev/null
+++ b/src/lib/constant.js
@@ -0,0 +1,153 @@
+export const BRAND_SOURCES = [
+ {
+ value: 'TKS Official',
+ name: 'TKS Official',
+ },
+ {
+ value: 'Third-party Agency',
+ name: 'Third-party Agency',
+ },
+ {
+ value: 'Offline Event',
+ name: 'Offline Event',
+ },
+ {
+ value: 'Social Media',
+ name: 'Social Media',
+ },
+];
+
+export const CAMPAIGN_SERVICES = [
+ {
+ value: 'fufei',
+ name: '达人短视频(付费)',
+ },
+ {
+ value: 'chunyong',
+ name: '达人短视频(纯佣)',
+ },
+ {
+ value: 'dai',
+ name: '直播(代播)',
+ },
+ {
+ value: 'dabao',
+ name: '直播(达播)',
+ },
+ {
+ value: 'chun',
+ name: '纯素材短视频',
+ },
+];
+
+export const CREATOR_TYPES = [
+ {
+ value: 'dai',
+ name: '带货类达人',
+ },
+ {
+ value: 'exposure',
+ name: '曝光类达人',
+ },
+];
+
+export const GMV_RANGES = [
+ {
+ value: '0-5k',
+ name: '$0 - $5K',
+ },
+ {
+ value: '5k-25k',
+ name: '$5K - $25K',
+ },
+ {
+ value: '25k-60k',
+ name: '$25K - $60K',
+ },
+ {
+ value: '60k-150k',
+ name: '$60K - $150K',
+ },
+ {
+ value: '150k-400k',
+ name: '$150K - $400K',
+ },
+ {
+ value: '400k-1500k',
+ name: '$400K - $1500K',
+ },
+ {
+ value: '1500k+',
+ name: '$1500K+',
+ },
+];
+
+export const CREATOR_LEVELS = [
+ {
+ value: 'L1',
+ name: 'L1',
+ },
+ {
+ value: 'L2',
+ name: 'L2',
+ },
+ {
+ value: 'L3',
+ name: 'L3',
+ },
+ {
+ value: 'L4',
+ name: 'L4',
+ },
+ {
+ value: 'L5',
+ name: 'L5',
+ },
+ {
+ value: 'L6',
+ name: 'L6',
+ },
+ {
+ value: 'L7',
+ name: 'L7',
+ },
+];
+
+export const CREATOR_CATEGORIES = [
+ {
+ value: 'Phones & Electronics',
+ name: 'Phones & Electronics',
+ },
+ {
+ value: 'Womenswear & Underwear',
+ name: 'Womenswear & Underwear',
+ },
+ {
+ value: 'Sports & Outdoor',
+ name: 'Sports & Outdoor',
+ },
+ {
+ value: 'Food & Beverage',
+ name: 'Food & Beverage',
+ },
+ {
+ value: 'Health',
+ name: 'Health',
+ },
+ {
+ value: 'Kitchenware',
+ name: 'Kitchenware',
+ },
+ {
+ value: 'Household Appliances',
+ name: 'Household Appliances',
+ },
+ {
+ value: 'Womensware & Underwear',
+ name: 'Womensware & Underwear',
+ },
+ {
+ value: 'Other',
+ name: 'Other',
+ },
+];
diff --git a/src/pages/Brands.jsx b/src/pages/Brands.jsx
index 2d7314a..ef7f5da 100644
--- a/src/pages/Brands.jsx
+++ b/src/pages/Brands.jsx
@@ -8,6 +8,7 @@ import { Plus } from 'lucide-react';
import { useDispatch, useSelector } from 'react-redux';
import { createBrandThunk, fetchBrands, selectBrand } from '../store/slices/brandsSlice';
import SpinningComponent from '../components/Spinning';
+import { BRAND_SOURCES } from '../lib/constant';
export default function Brands() {
const navigate = useNavigate();
@@ -48,13 +49,6 @@ function AddBrandModal({ show, onHide }) {
const dispatch = useDispatch();
const { status, error } = useSelector((state) => state.brands);
- const sourceOptions = [
- { value: '1', label: 'TKS Official' },
- { value: '2', label: 'Third Party Agency' },
- { value: '3', label: 'Official Event' },
- { value: '4', label: 'Social Media' },
- ];
-
const handleSubmit = async (e) => {
const form = document.getElementById('brandForm');
if (form.checkValidity() === false) {
@@ -112,7 +106,7 @@ function AddBrandModal({ show, onHide }) {
Source
setBrandSource(e.target.value)} required>
- {sourceOptions.map((option) => (
+ {BRAND_SOURCES.map((option) => (
diff --git a/src/pages/BrandsDetail.jsx b/src/pages/BrandsDetail.jsx
index ac359dd..ba1f642 100644
--- a/src/pages/BrandsDetail.jsx
+++ b/src/pages/BrandsDetail.jsx
@@ -1,14 +1,23 @@
import React, { useEffect, useState } from 'react';
import SearchBar from '../components/SearchBar';
-import { Button } from 'react-bootstrap';
+import { Alert, Button, Form, Modal } from 'react-bootstrap';
import { Folders, Hash, LinkIcon, Plus, Users } from 'lucide-react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useParams } from 'react-router-dom';
import CampaignList from '../components/CampaignList';
import ProductsList from '../components/ProductsList';
-import { fetchBrandDetail, fetchBrandCampaigns, fetchBrandProducts, setSelectedProduct } from '../store/slices/brandsSlice';
+import {
+ fetchBrandDetail,
+ fetchBrandCampaigns,
+ fetchBrandProducts,
+ setSelectedProduct,
+ createCampaignThunk,
+} from '../store/slices/brandsSlice';
import SlidePanel from '../components/SlidePanel';
import ProductDetail from '../components/ProductDetail';
+import { CAMPAIGN_SERVICES, CREATOR_CATEGORIES, CREATOR_LEVELS, CREATOR_TYPES, GMV_RANGES } from '../lib/constant';
+import RangeSlider from '../components/RangeSlider';
+import SpinningComponent from '../components/Spinning';
export default function BrandsDetail() {
const { id } = useParams();
@@ -16,6 +25,7 @@ export default function BrandsDetail() {
const [activeTab, setActiveTab] = useState('campaigns');
const { selectedBrand } = useSelector((state) => state.brands);
const [showProductDetail, setShowProductDetail] = useState(false);
+ const [showAddCampaignModal, setShowAddCampaignModal] = useState(false);
useEffect(() => {
if (id) {
@@ -30,14 +40,13 @@ export default function BrandsDetail() {
setShowProductDetail(true);
};
-
return (
selectedBrand?.id && (
{activeTab === 'campaigns' && (
-
-
+
setShowProductDetail(false)}
title='Product Detail'
size='xxl'
>
-
+
- setShowAddProductModal(false)} />
+ setShowAddProductModal(false)} />
>
)}
@@ -149,9 +151,26 @@ export default function CampaignDetail() {
);
}
-function AddProductModal({ show, onHide }) {
+function AddProductModal({ campaignId, show, onHide }) {
+ const [selectedProduct, setSelectedProduct] = useState(null);
+ const { products } = useSelector((state) => state.products);
+ const dispatch = useDispatch();
+
+ const handleCancel = () => {
+ onHide();
+ setSelectedProduct(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!selectedProduct) return;
+ console.log(selectedProduct);
+ await dispatch(addProductToCampaign({ campaignId, productId: selectedProduct })).unwrap();
+ dispatch(fetchCampaignDetail(campaignId));
+ handleCancel();
+ };
+
return (
-
+
Add Product
@@ -159,18 +178,26 @@ function AddProductModal({ show, onHide }) {
Product PID
-
+ setSelectedProduct(e.target.value)}
+ >
-
-
-
+ {products?.length > 0 && products.map((product) => (
+
+ ))}
-
+
Cancel
- Create
+
+ Create
+
diff --git a/src/pages/InboxTemplate.jsx b/src/pages/InboxTemplate.jsx
index c84550a..8861481 100644
--- a/src/pages/InboxTemplate.jsx
+++ b/src/pages/InboxTemplate.jsx
@@ -5,6 +5,7 @@ import React, { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { addTemplateApi, editTemplateApi, fetchTemplates } from '../store/slices/inboxSlice';
import TemplateList from '../components/TemplateList';
+import { CAMPAIGN_SERVICES } from '../lib/constant';
export default function InboxTemplate() {
const [activeTab, setActiveTab] = useState('all');
@@ -168,11 +169,9 @@ function AddTemplateModal({ show, formData, setFormData, handleClose, type = 'ad
-
-
-
-
-
+ {CAMPAIGN_SERVICES.map((service) => (
+
+ ))}
diff --git a/src/store/index.js b/src/store/index.js
index c033483..6302509 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -8,6 +8,7 @@ import inboxReducer from './slices/inboxSlice';
import authReducer from './slices/authSlice';
import discoveryReducer from './slices/discoverySlice';
import notificationBarReducer from './slices/notificationBarSlice';
+import productReducer from './slices/productSlice';
const authPersistConfig = {
key: 'auth',
@@ -22,6 +23,7 @@ const rootReducer = combineReducers({
discovery: discoveryReducer,
auth: persistReducer(authPersistConfig, authReducer),
notificationBar: notificationBarReducer,
+ products: productReducer,
});
const store = configureStore({
diff --git a/src/store/slices/brandsSlice.js b/src/store/slices/brandsSlice.js
index b2c402b..5bbf098 100644
--- a/src/store/slices/brandsSlice.js
+++ b/src/store/slices/brandsSlice.js
@@ -1,5 +1,6 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import api from '@/services/api';
+import { setNotificationBarMessage } from './notificationBarSlice';
const mockProducts = [
{
id: 1,
@@ -191,6 +192,20 @@ 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);
+ }
+ } catch (error) {
+ console.log(error);
+ dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
+ return rejectWithValue(error.message);
+ }
+});
+
const initialState = {
brands: [],
campaigns: [],
@@ -299,6 +314,16 @@ const brandsSlice = createSlice({
.addCase(fetchCampaignDetail.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
+ })
+ .addCase(createCampaignThunk.pending, (state) => {
+ state.status = 'loading';
+ })
+ .addCase(createCampaignThunk.fulfilled, (state, action) => {
+ state.status = 'succeeded';
+ })
+ .addCase(createCampaignThunk.rejected, (state, action) => {
+ state.status = 'failed';
+ state.error = action.error.message;
});
},
});
diff --git a/src/store/slices/productSlice.js b/src/store/slices/productSlice.js
new file mode 100644
index 0000000..0fe4c14
--- /dev/null
+++ b/src/store/slices/productSlice.js
@@ -0,0 +1,69 @@
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
+import api from '@/services/api';
+import { setNotificationBarMessage } from './notificationBarSlice';
+
+export const fetchProducts = createAsyncThunk('products/fetchProducts', async (_, { rejectWithValue, dispatch }) => {
+ try {
+ const response = await api.get('/products/');
+ if (response.code !== 200) {
+ throw new Error(response.message);
+ }
+ return response.data;
+ } catch (error) {
+ return rejectWithValue(error.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);
+ }
+ console.log(response);
+
+ return response.data;
+ } catch (error) {
+ dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
+ return rejectWithValue(error.message);
+ }
+});
+
+const initialState = {
+ products: [],
+ loading: false,
+ error: null,
+};
+
+const productSlice = createSlice({
+ name: 'products',
+ initialState,
+ reducers: {},
+ extraReducers: (builder) => {
+ builder.addCase(fetchProducts.pending, (state) => {
+ state.loading = true;
+ });
+ builder.addCase(fetchProducts.fulfilled, (state, action) => {
+ state.products = action.payload;
+ state.loading = false;
+ })
+ builder.addCase(fetchProducts.rejected, (state, action) => {
+ state.error = action.payload;
+ state.loading = false;
+ })
+ builder.addCase(addProductToCampaign.pending, (state) => {
+ state.loading = true;
+ })
+ builder.addCase(addProductToCampaign.fulfilled, (state, action) => {
+ state.products = action.payload;
+ state.loading = false;
+ })
+ builder.addCase(addProductToCampaign.rejected, (state, action) => {
+ state.error = action.payload;
+ state.loading = false;
+ })
+ },
+});
+
+export default productSlice.reducer;
diff --git a/src/styles/Brands.scss b/src/styles/Brands.scss
index 5276729..0eaf72b 100644
--- a/src/styles/Brands.scss
+++ b/src/styles/Brands.scss
@@ -217,7 +217,7 @@
.campaign-title {
font-size: 1.25rem;
font-weight: 800;
- margin-bottom: .5rem;
+ margin-bottom: 0.5rem;
color: $primary;
cursor: pointer;
}
@@ -268,29 +268,26 @@
flex-flow: row wrap;
gap: 1rem;
justify-content: space-between;
- height: 100%;
- overflow-y: auto;
-
+
.campaign-detail-info {
width: 100%;
background-color: #fff;
padding: 1.5rem;
border-radius: 0.375rem;
-
+
.campaign-info-top {
position: relative;
display: flex;
flex-flow: column nowrap;
align-items: flex-start;
gap: 0.25rem;
-
+
.campaign-name {
font-size: 1.25rem;
font-weight: 800;
color: $primary;
}
.campaign-descp {
-
}
.campaign-edit {
position: absolute;
@@ -303,7 +300,7 @@
gap: 0.25rem;
}
}
-
+
.campaign-info-bottom {
display: flex;
flex-flow: row wrap;
@@ -311,7 +308,7 @@
align-items: center;
gap: 0.5rem;
width: 100%;
-
+
.campaign-info-item {
width: 30%;
flex-shrink: 0;
@@ -320,7 +317,7 @@
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
-
+
.campaign-info-item-label {
color: $neutral-700;
width: 8.5rem;
@@ -328,11 +325,10 @@
align-items: center;
gap: 0.25rem;
}
-
}
}
}
-
+
.campaign-requirements {
width: 30%;
max-width: 380px;
@@ -358,7 +354,7 @@
}
}
}
-
+
.campaign-progress {
flex: 1;
background-color: #fff;
@@ -375,7 +371,7 @@
gap: 0.375rem;
flex: 1;
border-bottom: 4px solid transparent;
- padding: .375rem 0;
+ padding: 0.375rem 0;
.campaign-progress-item-index {
width: 1.75rem;
@@ -402,3 +398,16 @@
}
}
}
+
+#addCampaignForm {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+
+ div {
+ flex: 1;
+ .range-slider {
+ width: 90%;
+ }
+ }
+}
diff --git a/src/styles/DatabaseList.scss b/src/styles/DatabaseList.scss
index b30be13..5db0533 100644
--- a/src/styles/DatabaseList.scss
+++ b/src/styles/DatabaseList.scss
@@ -147,8 +147,6 @@
.table-container {
position: relative;
- max-height: calc(100% - 455px); // Adjust this value based on your layout
- overflow-y: auto;
.sticky-header {
position: sticky;
diff --git a/src/styles/custom-theme.scss b/src/styles/custom-theme.scss
index 599300c..d8ec45d 100644
--- a/src/styles/custom-theme.scss
+++ b/src/styles/custom-theme.scss
@@ -18,7 +18,7 @@ select:focus {
box-shadow: 0 0 0 0.2rem rgba(99, 102, 241, 0.25) !important;
outline: none;
}
-input:focus {
+.form-control:valid:focus {
box-shadow: 0 0 0 0.2rem rgba(99, 102, 241, 0.25) !important;
outline: none;
}
@@ -134,3 +134,8 @@ a {
box-shadow: 0px 0px 1px #171a1f12, 0px 0px 2px #171a1f1f; /* shadow-xs */
padding: 1rem;
}
+
+.modal-content {
+ width: max-content;
+ max-width: 60vw;
+}
\ No newline at end of file
diff --git a/src/styles/sidebar.scss b/src/styles/sidebar.scss
index 7283c26..1d0f019 100644
--- a/src/styles/sidebar.scss
+++ b/src/styles/sidebar.scss
@@ -151,7 +151,7 @@
transition: all 0.3s ease;
background: #f8f9fa;
border-radius: 8px;
- overflow: hidden;
+ overflow-y: auto;
}
// Collapsed sidebar adjustments
|