mirror of
https://github.com/Funkoala14/CreatorCenter_OOIN.git
synced 2025-06-07 12:28:15 +08:00
[dev]add campaign
This commit is contained in:
parent
0fa465a2c6
commit
ce1b944103
@ -4,7 +4,6 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { fetchCampaigns } from '../store/slices/brandsSlice';
|
||||
import SpinningComponent from './Spinning';
|
||||
import { addCreatorsToCampaign } from '../store/slices/creatorsSlice';
|
||||
import { setNotificationBarMessage } from '../store/slices/notificationBarSlice';
|
||||
|
||||
export default function AddToCampaign({ show, onHide }) {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -47,6 +47,9 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
// 预定义的离散点值
|
||||
const discreteValues = [0, 100, 1000, 10000, 100000, 250000, 500000];
|
||||
const discretePricingValues = [0, 200, 400, 600, 800, 1000, 3000];
|
||||
const marks = ['0', '100', '1k', '10k', '100k', '250k', '500k+'];
|
||||
const PricingMarks = ['0', '200', '400', '600', '800', '1000', '3000'];
|
||||
|
||||
// 找到最接近的离散值索引
|
||||
const findClosestDiscreteIndex = (value) => {
|
||||
let closestIndex = 0;
|
||||
@ -319,8 +322,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
<h5 className='filter-title'>Views</h5>
|
||||
<div className='filter-options filter-views filter-range-slider'>
|
||||
<RangeSlider
|
||||
min={0}
|
||||
max={500000}
|
||||
discreteValues={discreteValues}
|
||||
value={filters.views_range}
|
||||
onChange={handleViewsRangeChange}
|
||||
/>
|
||||
@ -359,8 +361,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
<h5 className='filter-title'>Pricing</h5>
|
||||
<div className='filter-options filter-pricing filter-range-slider'>
|
||||
<RangeSlider
|
||||
min={0}
|
||||
max={500000}
|
||||
discreteValues={discretePricingValues}
|
||||
value={filters.pricing}
|
||||
onChange={handlePricingRangeChange}
|
||||
/>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Check, CircleAlert, Info, X } from 'lucide-react';
|
||||
import { Check, CircleAlert, Info, ShieldAlert, X } from 'lucide-react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { resetNotificationBar } from '../store/slices/notificationBarSlice';
|
||||
import { useEffect } from 'react';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
|
||||
export default function NotificationBar() {
|
||||
const { message, type, show } = useSelector((state) => state.notificationBar);
|
||||
@ -24,18 +25,17 @@ export default function NotificationBar() {
|
||||
|
||||
return (
|
||||
show && (
|
||||
<div
|
||||
className={`snackbar alert alert-${type} d-flex align-items-center justify-content-between position-fixed top-5 start-50 translate-middle w-50 gap-2`}
|
||||
<Alert
|
||||
role='alert'
|
||||
style={{ zIndex: 999999, top: '15%' }}
|
||||
variant={type === 'error' ? 'danger' : type}
|
||||
style={{ zIndex: 999999, top: '10%', position: 'fixed', left: '50%', width: '60%', transform: 'translateX(-50%)', display: 'flex' }}
|
||||
>
|
||||
{type === 'success' && <Check />}
|
||||
{type === 'warning' && <CircleAlert />}
|
||||
{type === 'error' && <X />}
|
||||
{type === 'error' && <ShieldAlert />}
|
||||
{type === 'info' && <Info />}
|
||||
<div className='flex-fill'>{message}</div>
|
||||
<button type='button' className='btn-close flex-end' onClick={handleClose} aria-label='Close'></button>
|
||||
</div>
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,19 +1,15 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Table, Form } from 'react-bootstrap';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import '../styles/Products.scss';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import Spinning from './Spinning';
|
||||
|
||||
export default function ProductsList({ onShowProductDetail }) {
|
||||
const { brandId } = useParams();
|
||||
export default function ProductsList({ products, setSelectedProduct, onShowProductDetail }) {
|
||||
const { selectedBrand } = useSelector((state) => state.brands);
|
||||
const [products, setProducts] = useState([]);
|
||||
const [selectedProducts, setSelectedProducts] = useState([]);
|
||||
const [sortField, setSortField] = useState(null);
|
||||
const [sortDirection, setSortDirection] = useState('asc');
|
||||
|
||||
const [selectedProducts, setSelectedProducts] = useState([]);
|
||||
|
||||
const handleSort = (field) => {
|
||||
return;
|
||||
@ -60,7 +56,7 @@ export default function ProductsList({ onShowProductDetail }) {
|
||||
<th className='selector' style={{ width: '40px' }}>
|
||||
<Form.Check
|
||||
type='checkbox'
|
||||
checked={selectedProducts.length === selectedBrand?.products?.length && selectedBrand?.products?.length > 0}
|
||||
checked={selectedProducts.length === products?.length && products?.length > 0}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
</th>
|
||||
@ -94,14 +90,14 @@ export default function ProductsList({ onShowProductDetail }) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{selectedBrand?.products?.length === 0 ? (
|
||||
{products?.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan='10' className='text-center py-4'>
|
||||
No products found for this brand.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
selectedBrand?.products?.map((product) => (
|
||||
products?.map((product) => (
|
||||
<tr key={product.id} className={selectedProducts.includes(product.id) ? 'selected' : ''}>
|
||||
<td>
|
||||
<Form.Check
|
||||
@ -112,7 +108,8 @@ export default function ProductsList({ onShowProductDetail }) {
|
||||
</td>
|
||||
<td className='product-cell'>
|
||||
<div className='d-flex align-items-center' onClick={() => onShowProductDetail(product)} style={{cursor: 'pointer'}}>
|
||||
<div className='product-logo'>{product.name.slice(0, 1)}</div>
|
||||
<img className='product-logo' src={product.image_url} alt={product.name} />
|
||||
{/* <div className='product-logo'>{product.name.slice(0, 1)}</div> */}
|
||||
<div className='product-name'>{product.name}</div>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -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 }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className='range-slider-marks'>
|
||||
{marks.map((mark, index) => (
|
||||
<span key={index}>{mark}</span>
|
||||
{discreteValues.map((mark, index) => (
|
||||
<span key={index}>{formatValue(mark)}</span>
|
||||
))}
|
||||
</div>
|
||||
{/* 显示当前选中的值 */}
|
||||
|
153
src/lib/constant.js
Normal file
153
src/lib/constant.js
Normal file
@ -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',
|
||||
},
|
||||
];
|
@ -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 }) {
|
||||
<Form.Group className='mb-3' controlId='formBasicSource'>
|
||||
<Form.Label>Source</Form.Label>
|
||||
<Form.Select value={brandSource} onChange={(e) => setBrandSource(e.target.value)} required>
|
||||
{sourceOptions.map((option) => (
|
||||
{BRAND_SOURCES.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
|
@ -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 && (
|
||||
<React.Fragment>
|
||||
<div className='function-bar'>
|
||||
<SearchBar />
|
||||
{activeTab === 'campaigns' && (
|
||||
<Button>
|
||||
<Button onClick={() => setShowAddCampaignModal(true)}>
|
||||
<Plus />
|
||||
Add Campaign
|
||||
</Button>
|
||||
@ -125,7 +134,10 @@ export default function BrandsDetail() {
|
||||
{activeTab === 'campaigns' && <CampaignList />}
|
||||
{activeTab === 'products' && (
|
||||
<>
|
||||
<ProductsList onShowProductDetail={handleShowProductDetail} />
|
||||
<ProductsList
|
||||
products={selectedBrand?.products}
|
||||
onShowProductDetail={handleShowProductDetail}
|
||||
/>
|
||||
<SlidePanel
|
||||
show={showProductDetail}
|
||||
onClose={() => setShowProductDetail(false)}
|
||||
@ -136,7 +148,223 @@ export default function BrandsDetail() {
|
||||
</SlidePanel>
|
||||
</>
|
||||
)}
|
||||
<AddCampaignModal show={showAddCampaignModal} onHide={() => setShowAddCampaignModal(false)} />
|
||||
</React.Fragment>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function AddCampaignModal({ show, onHide }) {
|
||||
const dispatch = useDispatch();
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
service: '',
|
||||
link_product: '',
|
||||
creator_type: '',
|
||||
creator_level: '',
|
||||
creator_count: 0,
|
||||
budget: [0, 200],
|
||||
creator_category: '',
|
||||
followers: [0, 100],
|
||||
gmv: '',
|
||||
views: [0, 100000],
|
||||
});
|
||||
const [validated, setValidated] = useState(false);
|
||||
const budgetDiscreteValues = [0, 200, 400, 600, 800, 1000, 3000];
|
||||
const followersDiscreteValues = [0, 100, 1000, 10000, 250000, 500000];
|
||||
const viewsDiscreteValues = [0, 50000, 100000, 300000, 500000, 1000000, 5000000];
|
||||
const [errors, setErrors] = useState({});
|
||||
const { status } = useSelector((state) => state.brands);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const form = document.getElementById('addCampaignForm');
|
||||
if (form.checkValidity() === false) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setValidated(true);
|
||||
return;
|
||||
}
|
||||
console.log(formData);
|
||||
dispatch(createCampaignThunk(formData));
|
||||
};
|
||||
|
||||
const handleViewsChange = (views) => {
|
||||
console.log(views);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
service: '',
|
||||
link_product: '',
|
||||
creator_type: '',
|
||||
creator_level: '',
|
||||
creator_count: 0,
|
||||
budget: [200, 600],
|
||||
creator_category: '',
|
||||
followers: [0, 100],
|
||||
gmv: '',
|
||||
views: [0, 100000],
|
||||
});
|
||||
setValidated(false);
|
||||
onHide();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show={show} onHide={handleClose} >
|
||||
<Modal.Header>
|
||||
<Modal.Title>Add Campaign</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form id='addCampaignForm' noValidate validated={validated} onSubmit={handleSubmit}>
|
||||
<Form.Group>
|
||||
<Form.Label>Campaign Name *</Form.Label>
|
||||
<Form.Control required type='text' name='name' value={formData.name} onChange={handleChange} />
|
||||
<Form.Control.Feedback type='invalid'>Please enter campaign name.</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Campaign Description *</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type='text'
|
||||
name='description'
|
||||
isInvalid={!!errors.description}
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Form.Control.Feedback type='invalid'>Please enter campaign description.</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Service *</Form.Label>
|
||||
<Form.Select required name='service' value={formData.service} onChange={handleChange}>
|
||||
<option value='' disabled>
|
||||
Select Service
|
||||
</option>
|
||||
{CAMPAIGN_SERVICES.map((service) => (
|
||||
<option key={service.value} value={service.value}>
|
||||
{service.name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
<Form.Control.Feedback type='invalid'>Please select a service.</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Link Product *</Form.Label>
|
||||
<Form.Control
|
||||
type='text'
|
||||
name='link_product'
|
||||
value={formData.link_product}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Creator Type *</Form.Label>
|
||||
<Form.Select required name='creator_type' value={formData.creator_type} onChange={handleChange}>
|
||||
<option value='' disabled>
|
||||
Select Creator Type
|
||||
</option>
|
||||
{CREATOR_TYPES.map((creatorType) => (
|
||||
<option key={creatorType.value} value={creatorType.value}>
|
||||
{creatorType.name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
<Form.Control.Feedback type='invalid'>Please select a creator type.</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Creator Level *</Form.Label>
|
||||
<Form.Select
|
||||
required
|
||||
name='creator_level'
|
||||
value={formData.creator_level}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value='' disabled>
|
||||
Select Creator Level
|
||||
</option>
|
||||
{CREATOR_LEVELS.map((creatorLevel) => (
|
||||
<option key={creatorLevel.value} value={creatorLevel.value}>
|
||||
{creatorLevel.name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label># Creators *</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type='number'
|
||||
name='creator_count'
|
||||
value={formData.creator_count}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Form.Control.Feedback type='invalid'>
|
||||
Please enter the number of creators.
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Budget *</Form.Label>
|
||||
<RangeSlider
|
||||
discreteValues={budgetDiscreteValues}
|
||||
value={formData.budget}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Creator Category</Form.Label>
|
||||
<Form.Select name='creator_category' value={formData.creator_category} onChange={handleChange}>
|
||||
<option value=''>Select Creator Category</option>
|
||||
{CREATOR_CATEGORIES.map((creatorCategory) => (
|
||||
<option key={creatorCategory.value} value={creatorCategory.value}>
|
||||
{creatorCategory.name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Followers</Form.Label>
|
||||
<RangeSlider
|
||||
discreteValues={followersDiscreteValues}
|
||||
value={formData.followers}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>GMV</Form.Label>
|
||||
<Form.Select name='gmv' value={formData.gmv} onChange={handleChange}>
|
||||
<option value=''>Select GMV</option>
|
||||
{GMV_RANGES.map((gmv) => (
|
||||
<option key={gmv.value} value={gmv.value}>
|
||||
{gmv.name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Avg. Video Views</Form.Label>
|
||||
<RangeSlider
|
||||
discreteValues={viewsDiscreteValues}
|
||||
value={formData.views}
|
||||
onChange={handleViewsChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant='outline-primary' className='border-0' onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='primary' type='submit' form='addCampaignForm'>
|
||||
Add Campaign
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@ -3,12 +3,13 @@ import { Link, useParams } from 'react-router-dom';
|
||||
import SearchBar from '../components/SearchBar';
|
||||
import { Button, Form, Modal } from 'react-bootstrap';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { fetchBrands, fetchBrandDetail, fetchCampaignDetail, setSelectedProduct } from '../store/slices/brandsSlice';
|
||||
import { fetchBrands, fetchBrandDetail, fetchCampaignDetail, setSelectedProduct } from '../store/slices/brandsSlice';
|
||||
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';
|
||||
|
||||
export default function CampaignDetail() {
|
||||
const { brandId, campaignId } = useParams();
|
||||
@ -29,6 +30,7 @@ export default function CampaignDetail() {
|
||||
dispatch(fetchCampaignDetail(campaignId));
|
||||
}
|
||||
}
|
||||
dispatch(fetchProducts());
|
||||
}, [dispatch, brandId, campaignId]);
|
||||
|
||||
const handleShowProductDetail = (product) => {
|
||||
@ -132,16 +134,16 @@ export default function CampaignDetail() {
|
||||
<Plus size={18} />
|
||||
Add Product
|
||||
</Button>
|
||||
<ProductsList onShowProductDetail={handleShowProductDetail} />
|
||||
<ProductsList products={selectedCampaign?.link_product_details} onShowProductDetail={handleShowProductDetail} />
|
||||
<SlidePanel
|
||||
show={showProductDetail}
|
||||
onClose={() => setShowProductDetail(false)}
|
||||
title='Product Detail'
|
||||
size='xxl'
|
||||
>
|
||||
<CampaignScript />
|
||||
<CampaignScript />
|
||||
</SlidePanel>
|
||||
<AddProductModal show={showAddProductModal} onHide={() => setShowAddProductModal(false)} />
|
||||
<AddProductModal campaignId={campaignId} show={showAddProductModal} onHide={() => setShowAddProductModal(false)} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@ -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 (
|
||||
<Modal show={show} onHide={onHide}>
|
||||
<Modal show={show} onHide={handleCancel}>
|
||||
<Modal.Header closeButton className='fw-bold'>
|
||||
Add Product
|
||||
</Modal.Header>
|
||||
@ -159,18 +178,26 @@ function AddProductModal({ show, onHide }) {
|
||||
<Form className='add-product-form'>
|
||||
<Form.Group className='mb-3'>
|
||||
<Form.Label>Product PID</Form.Label>
|
||||
<Form.Select aria-label='Default select example'>
|
||||
<Form.Select
|
||||
aria-label='Default select example'
|
||||
value={selectedProduct}
|
||||
onChange={(e) => setSelectedProduct(e.target.value)}
|
||||
>
|
||||
<option>Select</option>
|
||||
<option value='1'>One</option>
|
||||
<option value='2'>Two</option>
|
||||
<option value='3'>Three</option>
|
||||
{products?.length > 0 && products.map((product) => (
|
||||
<option key={product.id} value={product.id}>
|
||||
{product.name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
<div className='button-group'>
|
||||
<Button variant='outline-light' className='text-primary'>
|
||||
<Button variant='outline-light' className='text-primary' onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='primary'>Create</Button>
|
||||
<Button variant='primary' onClick={handleSubmit}>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
|
@ -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
|
||||
<option value='' disabled>
|
||||
Select service
|
||||
</option>
|
||||
<option value='fufei'>达人短视频(付费)</option>
|
||||
<option value='chunyong'>达人短视频(纯佣)</option>
|
||||
<option value='dai'>直播(代播)</option>
|
||||
<option value='dabao'>直播(达播)</option>
|
||||
<option value='chun'>纯素材短视频</option>
|
||||
{CAMPAIGN_SERVICES.map((service) => (
|
||||
<option value={service.value}>{service.name}</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
<Form.Group className='mb-3' controlId='formBasicEmail'>
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
69
src/store/slices/productSlice.js
Normal file
69
src/store/slices/productSlice.js
Normal file
@ -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;
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
@ -151,7 +151,7 @@
|
||||
transition: all 0.3s ease;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
// Collapsed sidebar adjustments
|
||||
|
Loading…
Reference in New Issue
Block a user