mirror of
https://github.com/Funkoala14/CreatorCenter_OOIN.git
synced 2025-06-08 05:28:14 +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 { fetchCampaigns } from '../store/slices/brandsSlice';
|
||||||
import SpinningComponent from './Spinning';
|
import SpinningComponent from './Spinning';
|
||||||
import { addCreatorsToCampaign } from '../store/slices/creatorsSlice';
|
import { addCreatorsToCampaign } from '../store/slices/creatorsSlice';
|
||||||
import { setNotificationBarMessage } from '../store/slices/notificationBarSlice';
|
|
||||||
|
|
||||||
export default function AddToCampaign({ show, onHide }) {
|
export default function AddToCampaign({ show, onHide }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
@ -47,6 +47,9 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
|||||||
// 预定义的离散点值
|
// 预定义的离散点值
|
||||||
const discreteValues = [0, 100, 1000, 10000, 100000, 250000, 500000];
|
const discreteValues = [0, 100, 1000, 10000, 100000, 250000, 500000];
|
||||||
const discretePricingValues = [0, 200, 400, 600, 800, 1000, 3000];
|
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) => {
|
const findClosestDiscreteIndex = (value) => {
|
||||||
let closestIndex = 0;
|
let closestIndex = 0;
|
||||||
@ -319,8 +322,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
|||||||
<h5 className='filter-title'>Views</h5>
|
<h5 className='filter-title'>Views</h5>
|
||||||
<div className='filter-options filter-views filter-range-slider'>
|
<div className='filter-options filter-views filter-range-slider'>
|
||||||
<RangeSlider
|
<RangeSlider
|
||||||
min={0}
|
discreteValues={discreteValues}
|
||||||
max={500000}
|
|
||||||
value={filters.views_range}
|
value={filters.views_range}
|
||||||
onChange={handleViewsRangeChange}
|
onChange={handleViewsRangeChange}
|
||||||
/>
|
/>
|
||||||
@ -359,8 +361,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
|||||||
<h5 className='filter-title'>Pricing</h5>
|
<h5 className='filter-title'>Pricing</h5>
|
||||||
<div className='filter-options filter-pricing filter-range-slider'>
|
<div className='filter-options filter-pricing filter-range-slider'>
|
||||||
<RangeSlider
|
<RangeSlider
|
||||||
min={0}
|
discreteValues={discretePricingValues}
|
||||||
max={500000}
|
|
||||||
value={filters.pricing}
|
value={filters.pricing}
|
||||||
onChange={handlePricingRangeChange}
|
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 { useDispatch, useSelector } from 'react-redux';
|
||||||
import { resetNotificationBar } from '../store/slices/notificationBarSlice';
|
import { resetNotificationBar } from '../store/slices/notificationBarSlice';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { Alert } from 'react-bootstrap';
|
||||||
|
|
||||||
export default function NotificationBar() {
|
export default function NotificationBar() {
|
||||||
const { message, type, show } = useSelector((state) => state.notificationBar);
|
const { message, type, show } = useSelector((state) => state.notificationBar);
|
||||||
@ -24,18 +25,17 @@ export default function NotificationBar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
show && (
|
show && (
|
||||||
<div
|
<Alert
|
||||||
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`}
|
|
||||||
role='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 === 'success' && <Check />}
|
||||||
{type === 'warning' && <CircleAlert />}
|
{type === 'warning' && <CircleAlert />}
|
||||||
{type === 'error' && <X />}
|
{type === 'error' && <ShieldAlert />}
|
||||||
{type === 'info' && <Info />}
|
{type === 'info' && <Info />}
|
||||||
<div className='flex-fill'>{message}</div>
|
<div className='flex-fill'>{message}</div>
|
||||||
<button type='button' className='btn-close flex-end' onClick={handleClose} aria-label='Close'></button>
|
</Alert>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,15 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Table, Form } from 'react-bootstrap';
|
import { Table, Form } from 'react-bootstrap';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import '../styles/Products.scss';
|
import '../styles/Products.scss';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import Spinning from './Spinning';
|
import Spinning from './Spinning';
|
||||||
|
|
||||||
export default function ProductsList({ onShowProductDetail }) {
|
export default function ProductsList({ products, setSelectedProduct, onShowProductDetail }) {
|
||||||
const { brandId } = useParams();
|
|
||||||
const { selectedBrand } = useSelector((state) => state.brands);
|
const { selectedBrand } = useSelector((state) => state.brands);
|
||||||
const [products, setProducts] = useState([]);
|
|
||||||
const [selectedProducts, setSelectedProducts] = useState([]);
|
|
||||||
const [sortField, setSortField] = useState(null);
|
const [sortField, setSortField] = useState(null);
|
||||||
const [sortDirection, setSortDirection] = useState('asc');
|
const [sortDirection, setSortDirection] = useState('asc');
|
||||||
|
const [selectedProducts, setSelectedProducts] = useState([]);
|
||||||
|
|
||||||
const handleSort = (field) => {
|
const handleSort = (field) => {
|
||||||
return;
|
return;
|
||||||
@ -60,7 +56,7 @@ export default function ProductsList({ onShowProductDetail }) {
|
|||||||
<th className='selector' style={{ width: '40px' }}>
|
<th className='selector' style={{ width: '40px' }}>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
type='checkbox'
|
type='checkbox'
|
||||||
checked={selectedProducts.length === selectedBrand?.products?.length && selectedBrand?.products?.length > 0}
|
checked={selectedProducts.length === products?.length && products?.length > 0}
|
||||||
onChange={handleSelectAll}
|
onChange={handleSelectAll}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
@ -94,14 +90,14 @@ export default function ProductsList({ onShowProductDetail }) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{selectedBrand?.products?.length === 0 ? (
|
{products?.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan='10' className='text-center py-4'>
|
<td colSpan='10' className='text-center py-4'>
|
||||||
No products found for this brand.
|
No products found for this brand.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
selectedBrand?.products?.map((product) => (
|
products?.map((product) => (
|
||||||
<tr key={product.id} className={selectedProducts.includes(product.id) ? 'selected' : ''}>
|
<tr key={product.id} className={selectedProducts.includes(product.id) ? 'selected' : ''}>
|
||||||
<td>
|
<td>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
@ -112,7 +108,8 @@ export default function ProductsList({ onShowProductDetail }) {
|
|||||||
</td>
|
</td>
|
||||||
<td className='product-cell'>
|
<td className='product-cell'>
|
||||||
<div className='d-flex align-items-center' onClick={() => onShowProductDetail(product)} style={{cursor: 'pointer'}}>
|
<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 className='product-name'>{product.name}</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -2,9 +2,9 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||||||
import '../styles/RangeSlider.scss';
|
import '../styles/RangeSlider.scss';
|
||||||
import debounce from 'lodash/debounce';
|
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+'];
|
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>
|
</div>
|
||||||
<div className='range-slider-marks'>
|
<div className='range-slider-marks'>
|
||||||
{marks.map((mark, index) => (
|
{discreteValues.map((mark, index) => (
|
||||||
<span key={index}>{mark}</span>
|
<span key={index}>{formatValue(mark)}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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 { useDispatch, useSelector } from 'react-redux';
|
||||||
import { createBrandThunk, fetchBrands, selectBrand } from '../store/slices/brandsSlice';
|
import { createBrandThunk, fetchBrands, selectBrand } from '../store/slices/brandsSlice';
|
||||||
import SpinningComponent from '../components/Spinning';
|
import SpinningComponent from '../components/Spinning';
|
||||||
|
import { BRAND_SOURCES } from '../lib/constant';
|
||||||
|
|
||||||
export default function Brands() {
|
export default function Brands() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -48,13 +49,6 @@ function AddBrandModal({ show, onHide }) {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { status, error } = useSelector((state) => state.brands);
|
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 handleSubmit = async (e) => {
|
||||||
const form = document.getElementById('brandForm');
|
const form = document.getElementById('brandForm');
|
||||||
if (form.checkValidity() === false) {
|
if (form.checkValidity() === false) {
|
||||||
@ -112,7 +106,7 @@ function AddBrandModal({ show, onHide }) {
|
|||||||
<Form.Group className='mb-3' controlId='formBasicSource'>
|
<Form.Group className='mb-3' controlId='formBasicSource'>
|
||||||
<Form.Label>Source</Form.Label>
|
<Form.Label>Source</Form.Label>
|
||||||
<Form.Select value={brandSource} onChange={(e) => setBrandSource(e.target.value)} required>
|
<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 key={option.value} value={option.value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</option>
|
</option>
|
||||||
|
@ -1,14 +1,23 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import SearchBar from '../components/SearchBar';
|
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 { Folders, Hash, LinkIcon, Plus, Users } from 'lucide-react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import CampaignList from '../components/CampaignList';
|
import CampaignList from '../components/CampaignList';
|
||||||
import ProductsList from '../components/ProductsList';
|
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 SlidePanel from '../components/SlidePanel';
|
||||||
import ProductDetail from '../components/ProductDetail';
|
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() {
|
export default function BrandsDetail() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
@ -16,6 +25,7 @@ export default function BrandsDetail() {
|
|||||||
const [activeTab, setActiveTab] = useState('campaigns');
|
const [activeTab, setActiveTab] = useState('campaigns');
|
||||||
const { selectedBrand } = useSelector((state) => state.brands);
|
const { selectedBrand } = useSelector((state) => state.brands);
|
||||||
const [showProductDetail, setShowProductDetail] = useState(false);
|
const [showProductDetail, setShowProductDetail] = useState(false);
|
||||||
|
const [showAddCampaignModal, setShowAddCampaignModal] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
@ -30,14 +40,13 @@ export default function BrandsDetail() {
|
|||||||
setShowProductDetail(true);
|
setShowProductDetail(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
selectedBrand?.id && (
|
selectedBrand?.id && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className='function-bar'>
|
<div className='function-bar'>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
{activeTab === 'campaigns' && (
|
{activeTab === 'campaigns' && (
|
||||||
<Button>
|
<Button onClick={() => setShowAddCampaignModal(true)}>
|
||||||
<Plus />
|
<Plus />
|
||||||
Add Campaign
|
Add Campaign
|
||||||
</Button>
|
</Button>
|
||||||
@ -125,7 +134,10 @@ export default function BrandsDetail() {
|
|||||||
{activeTab === 'campaigns' && <CampaignList />}
|
{activeTab === 'campaigns' && <CampaignList />}
|
||||||
{activeTab === 'products' && (
|
{activeTab === 'products' && (
|
||||||
<>
|
<>
|
||||||
<ProductsList onShowProductDetail={handleShowProductDetail} />
|
<ProductsList
|
||||||
|
products={selectedBrand?.products}
|
||||||
|
onShowProductDetail={handleShowProductDetail}
|
||||||
|
/>
|
||||||
<SlidePanel
|
<SlidePanel
|
||||||
show={showProductDetail}
|
show={showProductDetail}
|
||||||
onClose={() => setShowProductDetail(false)}
|
onClose={() => setShowProductDetail(false)}
|
||||||
@ -136,7 +148,223 @@ export default function BrandsDetail() {
|
|||||||
</SlidePanel>
|
</SlidePanel>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<AddCampaignModal show={showAddCampaignModal} onHide={() => setShowAddCampaignModal(false)} />
|
||||||
</React.Fragment>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@ 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';
|
||||||
|
|
||||||
export default function CampaignDetail() {
|
export default function CampaignDetail() {
|
||||||
const { brandId, campaignId } = useParams();
|
const { brandId, campaignId } = useParams();
|
||||||
@ -29,6 +30,7 @@ export default function CampaignDetail() {
|
|||||||
dispatch(fetchCampaignDetail(campaignId));
|
dispatch(fetchCampaignDetail(campaignId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dispatch(fetchProducts());
|
||||||
}, [dispatch, brandId, campaignId]);
|
}, [dispatch, brandId, campaignId]);
|
||||||
|
|
||||||
const handleShowProductDetail = (product) => {
|
const handleShowProductDetail = (product) => {
|
||||||
@ -132,7 +134,7 @@ export default function CampaignDetail() {
|
|||||||
<Plus size={18} />
|
<Plus size={18} />
|
||||||
Add Product
|
Add Product
|
||||||
</Button>
|
</Button>
|
||||||
<ProductsList onShowProductDetail={handleShowProductDetail} />
|
<ProductsList products={selectedCampaign?.link_product_details} onShowProductDetail={handleShowProductDetail} />
|
||||||
<SlidePanel
|
<SlidePanel
|
||||||
show={showProductDetail}
|
show={showProductDetail}
|
||||||
onClose={() => setShowProductDetail(false)}
|
onClose={() => setShowProductDetail(false)}
|
||||||
@ -141,7 +143,7 @@ export default function CampaignDetail() {
|
|||||||
>
|
>
|
||||||
<CampaignScript />
|
<CampaignScript />
|
||||||
</SlidePanel>
|
</SlidePanel>
|
||||||
<AddProductModal show={showAddProductModal} onHide={() => setShowAddProductModal(false)} />
|
<AddProductModal campaignId={campaignId} show={showAddProductModal} onHide={() => setShowAddProductModal(false)} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 (
|
return (
|
||||||
<Modal show={show} onHide={onHide}>
|
<Modal show={show} onHide={handleCancel}>
|
||||||
<Modal.Header closeButton className='fw-bold'>
|
<Modal.Header closeButton className='fw-bold'>
|
||||||
Add Product
|
Add Product
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
@ -159,18 +178,26 @@ function AddProductModal({ show, onHide }) {
|
|||||||
<Form className='add-product-form'>
|
<Form className='add-product-form'>
|
||||||
<Form.Group className='mb-3'>
|
<Form.Group className='mb-3'>
|
||||||
<Form.Label>Product PID</Form.Label>
|
<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>Select</option>
|
||||||
<option value='1'>One</option>
|
{products?.length > 0 && products.map((product) => (
|
||||||
<option value='2'>Two</option>
|
<option key={product.id} value={product.id}>
|
||||||
<option value='3'>Three</option>
|
{product.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
</Form.Select>
|
</Form.Select>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<div className='button-group'>
|
<div className='button-group'>
|
||||||
<Button variant='outline-light' className='text-primary'>
|
<Button variant='outline-light' className='text-primary' onClick={handleCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='primary'>Create</Button>
|
<Button variant='primary' onClick={handleSubmit}>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
|
@ -5,6 +5,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { addTemplateApi, editTemplateApi, fetchTemplates } from '../store/slices/inboxSlice';
|
import { addTemplateApi, editTemplateApi, fetchTemplates } from '../store/slices/inboxSlice';
|
||||||
import TemplateList from '../components/TemplateList';
|
import TemplateList from '../components/TemplateList';
|
||||||
|
import { CAMPAIGN_SERVICES } from '../lib/constant';
|
||||||
|
|
||||||
export default function InboxTemplate() {
|
export default function InboxTemplate() {
|
||||||
const [activeTab, setActiveTab] = useState('all');
|
const [activeTab, setActiveTab] = useState('all');
|
||||||
@ -168,11 +169,9 @@ function AddTemplateModal({ show, formData, setFormData, handleClose, type = 'ad
|
|||||||
<option value='' disabled>
|
<option value='' disabled>
|
||||||
Select service
|
Select service
|
||||||
</option>
|
</option>
|
||||||
<option value='fufei'>达人短视频(付费)</option>
|
{CAMPAIGN_SERVICES.map((service) => (
|
||||||
<option value='chunyong'>达人短视频(纯佣)</option>
|
<option value={service.value}>{service.name}</option>
|
||||||
<option value='dai'>直播(代播)</option>
|
))}
|
||||||
<option value='dabao'>直播(达播)</option>
|
|
||||||
<option value='chun'>纯素材短视频</option>
|
|
||||||
</Form.Select>
|
</Form.Select>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group className='mb-3' controlId='formBasicEmail'>
|
<Form.Group className='mb-3' controlId='formBasicEmail'>
|
||||||
|
@ -8,6 +8,7 @@ import inboxReducer from './slices/inboxSlice';
|
|||||||
import authReducer from './slices/authSlice';
|
import authReducer from './slices/authSlice';
|
||||||
import discoveryReducer from './slices/discoverySlice';
|
import discoveryReducer from './slices/discoverySlice';
|
||||||
import notificationBarReducer from './slices/notificationBarSlice';
|
import notificationBarReducer from './slices/notificationBarSlice';
|
||||||
|
import productReducer from './slices/productSlice';
|
||||||
|
|
||||||
const authPersistConfig = {
|
const authPersistConfig = {
|
||||||
key: 'auth',
|
key: 'auth',
|
||||||
@ -22,6 +23,7 @@ const rootReducer = combineReducers({
|
|||||||
discovery: discoveryReducer,
|
discovery: discoveryReducer,
|
||||||
auth: persistReducer(authPersistConfig, authReducer),
|
auth: persistReducer(authPersistConfig, authReducer),
|
||||||
notificationBar: notificationBarReducer,
|
notificationBar: notificationBarReducer,
|
||||||
|
products: productReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||||
import api from '@/services/api';
|
import api from '@/services/api';
|
||||||
|
import { setNotificationBarMessage } from './notificationBarSlice';
|
||||||
const mockProducts = [
|
const mockProducts = [
|
||||||
{
|
{
|
||||||
id: 1,
|
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 = {
|
const initialState = {
|
||||||
brands: [],
|
brands: [],
|
||||||
campaigns: [],
|
campaigns: [],
|
||||||
@ -299,6 +314,16 @@ const brandsSlice = createSlice({
|
|||||||
.addCase(fetchCampaignDetail.rejected, (state, action) => {
|
.addCase(fetchCampaignDetail.rejected, (state, action) => {
|
||||||
state.status = 'failed';
|
state.status = 'failed';
|
||||||
state.error = action.error.message;
|
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 {
|
.campaign-title {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
margin-bottom: .5rem;
|
margin-bottom: 0.5rem;
|
||||||
color: $primary;
|
color: $primary;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -268,8 +268,6 @@
|
|||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.campaign-detail-info {
|
.campaign-detail-info {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -290,7 +288,6 @@
|
|||||||
color: $primary;
|
color: $primary;
|
||||||
}
|
}
|
||||||
.campaign-descp {
|
.campaign-descp {
|
||||||
|
|
||||||
}
|
}
|
||||||
.campaign-edit {
|
.campaign-edit {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -328,7 +325,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -375,7 +371,7 @@
|
|||||||
gap: 0.375rem;
|
gap: 0.375rem;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border-bottom: 4px solid transparent;
|
border-bottom: 4px solid transparent;
|
||||||
padding: .375rem 0;
|
padding: 0.375rem 0;
|
||||||
|
|
||||||
.campaign-progress-item-index {
|
.campaign-progress-item-index {
|
||||||
width: 1.75rem;
|
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 {
|
.table-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-height: calc(100% - 455px); // Adjust this value based on your layout
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.sticky-header {
|
.sticky-header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
@ -18,7 +18,7 @@ select:focus {
|
|||||||
box-shadow: 0 0 0 0.2rem rgba(99, 102, 241, 0.25) !important;
|
box-shadow: 0 0 0 0.2rem rgba(99, 102, 241, 0.25) !important;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
input:focus {
|
.form-control:valid:focus {
|
||||||
box-shadow: 0 0 0 0.2rem rgba(99, 102, 241, 0.25) !important;
|
box-shadow: 0 0 0 0.2rem rgba(99, 102, 241, 0.25) !important;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
@ -134,3 +134,8 @@ a {
|
|||||||
box-shadow: 0px 0px 1px #171a1f12, 0px 0px 2px #171a1f1f; /* shadow-xs */
|
box-shadow: 0px 0px 1px #171a1f12, 0px 0px 2px #171a1f1f; /* shadow-xs */
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: max-content;
|
||||||
|
max-width: 60vw;
|
||||||
|
}
|
@ -151,7 +151,7 @@
|
|||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collapsed sidebar adjustments
|
// Collapsed sidebar adjustments
|
||||||
|
Loading…
Reference in New Issue
Block a user