mirror of
https://github.com/Funkoala14/CreatorCenter_OOIN.git
synced 2025-06-08 02:58:14 +08:00
[dev]brands' campaign page
This commit is contained in:
parent
cfb82c19b7
commit
7b0d0a109e
18
package-lock.json
generated
18
package-lock.json
generated
@ -20,7 +20,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.508.0",
|
||||
"react": "^19.1.0",
|
||||
"react-bootstrap": "^2.10.1",
|
||||
"react-bootstrap": "^2.10.9",
|
||||
"react-bootstrap-range-slider": "^3.0.8",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
@ -1814,6 +1814,11 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
||||
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz",
|
||||
@ -3928,13 +3933,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-bootstrap": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.1.tgz",
|
||||
"integrity": "sha512-J3OpRZIvCTQK+Tg/jOkRUvpYLHMdGeU9KqFUBQrV0d/Qr/3nsINpiOJyZMWnM5SJ3ctZdhPA6eCIKpEJR3Ellg==",
|
||||
"version": "2.10.9",
|
||||
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.9.tgz",
|
||||
"integrity": "sha512-TJUCuHcxdgYpOqeWmRApM/Dy0+hVsxNRFvq2aRFQuxhNi/+ivOxC5OdWIeHS3agxvzJ4Ev4nDw2ZdBl9ymd/JQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.22.5",
|
||||
"@babel/runtime": "^7.24.7",
|
||||
"@restart/hooks": "^0.4.9",
|
||||
"@restart/ui": "^1.6.6",
|
||||
"@restart/ui": "^1.9.4",
|
||||
"@types/prop-types": "^15.7.12",
|
||||
"@types/react-transition-group": "^4.4.6",
|
||||
"classnames": "^2.3.2",
|
||||
"dom-helpers": "^5.2.1",
|
||||
|
@ -22,7 +22,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.508.0",
|
||||
"react": "^19.1.0",
|
||||
"react-bootstrap": "^2.10.1",
|
||||
"react-bootstrap": "^2.10.9",
|
||||
"react-bootstrap-range-slider": "^3.0.8",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
|
@ -4,7 +4,7 @@ import { fetchBrands } from '../store/slices/brandsSlice';
|
||||
import { Card } from 'react-bootstrap';
|
||||
import { Folders, Hash, Link, Users } from 'lucide-react';
|
||||
|
||||
export default function BrandsList() {
|
||||
export default function BrandsList({ openBrandDetail }) {
|
||||
const brands = useSelector((state) => state.brands.brands);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@ -14,8 +14,8 @@ export default function BrandsList() {
|
||||
|
||||
return (
|
||||
<div className='brands-list'>
|
||||
{brands.map((brand) => (
|
||||
<div className='brand-card shadow-xs' key={brand.id}>
|
||||
{brands?.length > 0 && brands.map((brand) => (
|
||||
<div className='brand-card shadow-xs' key={brand.id} onClick={() => openBrandDetail(brand)}>
|
||||
<Card.Body>
|
||||
<Card.Title className='text-primary fw-bold'>
|
||||
<span className='card-logo'>{brand.name.slice(0, 1)}</span>
|
||||
|
91
src/components/CampaignInfo.jsx
Normal file
91
src/components/CampaignInfo.jsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { ChartNoAxesColumnIncreasing, CircleDollarSign, Edit, Eye, Folders, Hash, Layers, Tag, TrendingUp, UserRoundCheck } from 'lucide-react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
export default function CampaignInfo() {
|
||||
const { selectedCampaign } = useSelector((state) => state.brands);
|
||||
return (
|
||||
<div className='campaign-detail-info shadow-xs'>
|
||||
<div className='campaign-info-top'>
|
||||
<div className='campaign-name'>{selectedCampaign.name}</div>
|
||||
<div className='campaign-descp'>{selectedCampaign.description || '--'}</div>
|
||||
<div className='campaign-edit'>
|
||||
<Edit size={18} />
|
||||
Edit
|
||||
</div>
|
||||
</div>
|
||||
<div className='campaign-info-bottom'>
|
||||
<div className='campaign-info-item'>
|
||||
<div className='campaign-info-item-label'>
|
||||
<Layers size={18} />
|
||||
Service
|
||||
</div>
|
||||
<div className='campaign-info-item-value'>{selectedCampaign?.service || '--'}</div>
|
||||
</div>
|
||||
<div className='campaign-info-item'>
|
||||
<div className='campaign-info-item-label'>
|
||||
<Folders size={18} />
|
||||
Category
|
||||
</div>
|
||||
<div className='campaign-info-item-value'>
|
||||
{selectedCampaign?.category?.length > 0 &&
|
||||
selectedCampaign.category.map((cat) => <span className='category-tag'>{cat}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='campaign-info-item'>
|
||||
<div className='campaign-info-item-label'>
|
||||
<UserRoundCheck size={18} />
|
||||
Followers
|
||||
</div>
|
||||
<div className='campaign-info-item-value'>{selectedCampaign?.followers || '--'}</div>
|
||||
</div>
|
||||
<div className='campaign-info-item'>
|
||||
<div className='campaign-info-item-label'>
|
||||
<Tag size={18} />
|
||||
Creator Category
|
||||
</div>
|
||||
<div className='campaign-info-item-value'>{selectedCampaign?.creator_category || '--'}</div>
|
||||
</div>
|
||||
<div className='campaign-info-item'>
|
||||
<div className='campaign-info-item-label'>
|
||||
<TrendingUp size={18} />
|
||||
GMV
|
||||
</div>
|
||||
<div className='campaign-info-item-value'>{selectedCampaign?.gmv || '--'}</div>
|
||||
</div>
|
||||
<div className='campaign-info-item'>
|
||||
<div className='campaign-info-item-label'>
|
||||
<CircleDollarSign size={18} />
|
||||
Pricing
|
||||
</div>
|
||||
<div className='campaign-info-item-value'>{selectedCampaign?.pricing || '--'}</div>
|
||||
</div>
|
||||
<div className='campaign-info-item'>
|
||||
<div className='campaign-info-item-label'>
|
||||
<ChartNoAxesColumnIncreasing size={18} />
|
||||
Creator Level
|
||||
</div>
|
||||
<div className='campaign-info-item-value'>
|
||||
{selectedCampaign?.creator_level?.length > 0 &&
|
||||
selectedCampaign.creator_level.map((level) => (
|
||||
<span className='creator-level-tag'>{level}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='campaign-info-item'>
|
||||
<div className='campaign-info-item-label'>
|
||||
<Eye size={18} />
|
||||
Views
|
||||
</div>
|
||||
<div className='campaign-info-item-value'>{selectedCampaign?.views || '--'}</div>
|
||||
</div>
|
||||
<div className='campaign-info-item'>
|
||||
<div className='campaign-info-item-label'>
|
||||
<Hash size={18} />
|
||||
Creators
|
||||
</div>
|
||||
<div className='campaign-info-item-value'>{selectedCampaign?.creators || '--'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
70
src/components/CampaignList.jsx
Normal file
70
src/components/CampaignList.jsx
Normal file
@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ChartNoAxesColumnIncreasing, CircleDollarSign, Edit, Eye, Folders, Hash, Layers, Tag, TrendingUp, UserRoundCheck } from 'lucide-react';
|
||||
|
||||
|
||||
export default function CampaignList() {
|
||||
const { selectedBrand } = useSelector((state) => state.brands);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(selectedBrand);
|
||||
}, [selectedBrand]);
|
||||
return (
|
||||
<div className='campaigns-list'>
|
||||
{selectedBrand?.campaigns?.length > 0 &&
|
||||
selectedBrand.campaigns.map((campaign) => (
|
||||
<div className='campaign-info'>
|
||||
<Link to={`/brands/${selectedBrand.id}/campaigns/${campaign.id}`} className='campaign-title'>
|
||||
{campaign.name}
|
||||
</Link>
|
||||
<div className='campaign-item'>
|
||||
<div className='campaign-item-label'><Layers size={18} />Service</div>
|
||||
<div className='campaign-item-value'>{campaign.service}</div>
|
||||
</div>
|
||||
<div className='campaign-item'>
|
||||
<div className='campaign-item-label'>Creator Type</div>
|
||||
<div className='campaign-item-value'>{campaign.creatorType}</div>
|
||||
</div>
|
||||
<div className='campaign-item'>
|
||||
<div className='campaign-item-label'><Hash size={18} />Creators</div>
|
||||
<div className='campaign-item-value'>{campaign.creators}</div>
|
||||
</div>
|
||||
<div className='campaign-item'>
|
||||
<div className='campaign-item-label'><ChartNoAxesColumnIncreasing size={18} />Creator Level</div>
|
||||
<div className='campaign-item-value'>
|
||||
{campaign.creator_level &&
|
||||
campaign.creator_level.map((level) => (
|
||||
<span className='creator-level-tag'>{level}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='campaign-item'>
|
||||
<div className='campaign-item-label'><Folders size={18} />Category</div>
|
||||
<div className='campaign-item-value'>
|
||||
{campaign.category &&
|
||||
campaign.category.map((cat) => <span className='category-tag'>{cat}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='campaign-item'>
|
||||
<div className='campaign-item-label'><TrendingUp size={18} />GMV</div>
|
||||
<div className='campaign-item-value'>{campaign.gmv}</div>
|
||||
</div>
|
||||
<div className='campaign-item'>
|
||||
<div className='campaign-item-label'><UserRoundCheck size={18} />Followers</div>
|
||||
<div className='campaign-item-value'>{campaign.followers}</div>
|
||||
</div>
|
||||
<div className='campaign-item'>
|
||||
<div className='campaign-item-label'><Eye size={18} />Views</div>
|
||||
<div className='campaign-item-value'>{campaign.views}</div>
|
||||
</div>
|
||||
<div className='campaign-item'>
|
||||
<div className='campaign-item-label'><CircleDollarSign size={18} />Budget</div>
|
||||
<div className='campaign-item-value'>{campaign.budget}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -10,6 +10,7 @@ import {
|
||||
} from '../store/slices/creatorsSlice';
|
||||
import { setSortBy } from '../store/slices/filtersSlice';
|
||||
import '../styles/DatabaseList.scss';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
export default function DatabaseList({ path }) {
|
||||
const dispatch = useDispatch();
|
||||
@ -175,7 +176,7 @@ export default function DatabaseList({ path }) {
|
||||
<td className='text-center'>
|
||||
{creator.hasTiktok && (
|
||||
<div className='social-icon tiktok-icon mx-auto'>
|
||||
<i className='fab fa-tiktok'></i>
|
||||
<FontAwesomeIcon icon='fa-brands fa-tiktok' />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
|
@ -144,15 +144,15 @@ export default function Sidebar() {
|
||||
|
||||
return (
|
||||
<div className={`sidebar ${collapsed ? 'sidebar-collapsed' : ''}`}>
|
||||
<div className='sidebar-header p-3 d-flex align-items-center'>
|
||||
<Link to='/' className='sidebar-header p-3 d-flex align-items-center'>
|
||||
<img src={logo} alt='OOIN Logo' width={48} height={48} className='me-2' />
|
||||
{!collapsed && (
|
||||
<div>
|
||||
<div className='fw-bold'>OOIN Media</div>
|
||||
<div className='text-black fw-bold'>OOIN Media</div>
|
||||
<div className='small text-muted'>Creator Center</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Nav className='flex-column sidebar-nav'>
|
||||
{menuItems.map((item) => {
|
||||
|
140
src/components/ProductsList.jsx
Normal file
140
src/components/ProductsList.jsx
Normal file
@ -0,0 +1,140 @@
|
||||
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';
|
||||
|
||||
export default function ProductsList() {
|
||||
const { brandId } = useParams();
|
||||
const { brands } = useSelector((state) => state.brands);
|
||||
const [products, setProducts] = useState([]);
|
||||
const [selectedProducts, setSelectedProducts] = useState([]);
|
||||
const [sortField, setSortField] = useState(null);
|
||||
const [sortDirection, setSortDirection] = useState('asc');
|
||||
|
||||
useEffect(() => {
|
||||
if (brands.length > 0) {
|
||||
const brand = brands.find((b) => b.id.toString() === brandId);
|
||||
if (brand && brand.products) {
|
||||
setProducts(brand.products);
|
||||
}
|
||||
}
|
||||
}, [brands, brandId]);
|
||||
|
||||
const handleSort = (field) => {
|
||||
if (sortField === field) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const renderSortIcon = (field) => {
|
||||
if (sortField !== field) return null;
|
||||
return sortDirection === 'asc' ? '↑' : '↓';
|
||||
};
|
||||
|
||||
const handleSelectAll = (e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedProducts(products.map((product) => product.id));
|
||||
} else {
|
||||
setSelectedProducts([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectProduct = (productId) => {
|
||||
if (selectedProducts.includes(productId)) {
|
||||
setSelectedProducts(selectedProducts.filter((id) => id !== productId));
|
||||
} else {
|
||||
setSelectedProducts([...selectedProducts, productId]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='products-list'>
|
||||
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className='selector' style={{ width: '40px' }}>
|
||||
<Form.Check
|
||||
type='checkbox'
|
||||
checked={selectedProducts.length === products.length && products.length > 0}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
</th>
|
||||
<th className='product' onClick={() => handleSort('name')}>
|
||||
Product {renderSortIcon('name')}
|
||||
</th>
|
||||
<th className='commission text-center' onClick={() => handleSort('commission')}>
|
||||
Commission Rate {renderSortIcon('commission')}
|
||||
</th>
|
||||
<th className='samples text-center' onClick={() => handleSort('availableSamples')}>
|
||||
Available Samples {renderSortIcon('availableSamples')}
|
||||
</th>
|
||||
<th className='price text-center' onClick={() => handleSort('price')}>
|
||||
Sales Price {renderSortIcon('price')}
|
||||
</th>
|
||||
<th className='stock text-center' onClick={() => handleSort('stock')}>
|
||||
Stock {renderSortIcon('stock')}
|
||||
</th>
|
||||
<th className='sold text-center' onClick={() => handleSort('sold')}>
|
||||
Items Sold {renderSortIcon('sold')}
|
||||
</th>
|
||||
<th className='rating text-center' onClick={() => handleSort('rating')}>
|
||||
Product Rating {renderSortIcon('rating')}
|
||||
</th>
|
||||
<th className='creators text-center' onClick={() => handleSort('collabCreators')}>
|
||||
Collab. Creators {renderSortIcon('collabCreators')}
|
||||
</th>
|
||||
<th className='tiktokShop text-center' onClick={() => handleSort('tiktokShop')}>
|
||||
TikTok Shop {renderSortIcon('tiktokShop')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{products.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan='10' className='text-center py-4'>
|
||||
No products found for this brand.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
products.map((product) => (
|
||||
<tr key={product.id} className={selectedProducts.includes(product.id) ? 'selected' : ''}>
|
||||
<td>
|
||||
<Form.Check
|
||||
type='checkbox'
|
||||
checked={selectedProducts.includes(product.id)}
|
||||
onChange={() => handleSelectProduct(product.id)}
|
||||
/>
|
||||
</td>
|
||||
<td className='product-cell'>
|
||||
<div className='d-flex align-items-center'>
|
||||
<div className='product-logo'>{product.name.slice(0, 1)}</div>
|
||||
<div className='product-name'>{product.name}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className='text-center' >
|
||||
<div>{product.commission}</div>
|
||||
<div className='small text-muted'>Open collab. {product.openCollab}</div>
|
||||
</td>
|
||||
<td className='text-center'>{product.availableSamples}</td>
|
||||
<td className='text-center'>{product.price}</td>
|
||||
<td className='text-center'>{product.stock}</td>
|
||||
<td className='text-center'>{product.sold}</td>
|
||||
<td className='text-center'>
|
||||
<div>{product.rating}</div>
|
||||
<div className='small text-muted'>{product.reviews} Reviews</div>
|
||||
</td>
|
||||
<td className='text-center'>{product.collabCreators}</td>
|
||||
<td className='text-center'>{product.tiktokShop && <FontAwesomeIcon icon='fa-brands fa-tiktok' />}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,21 +1,39 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import BrandsList from '../components/BrandsList';
|
||||
import SearchBar from '../components/SearchBar';
|
||||
import { Button, Modal, Form } from 'react-bootstrap';
|
||||
import '../styles/Brands.scss';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { selectBrand } from '../store/slices/brandsSlice';
|
||||
|
||||
export default function Brands() {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [showAddBrandModal, setShowAddBrandModal] = useState(false);
|
||||
|
||||
useEffect(() => {}, [dispatch]);
|
||||
|
||||
const openBrandDetail = async (item) => {
|
||||
console.log(item);
|
||||
await dispatch(selectBrand(item));
|
||||
navigate(`/brands/${item.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className='function-bar'>
|
||||
<SearchBar />
|
||||
<Button onClick={() => setShowAddBrandModal(true)}>+ Add Brand</Button>
|
||||
<Button onClick={() => setShowAddBrandModal(true)}>
|
||||
<Plus />
|
||||
Add Brand
|
||||
</Button>
|
||||
</div>
|
||||
<div className='breadcrumb'>
|
||||
<div className='breadcrumb-item'>Brands</div>
|
||||
</div>
|
||||
<BrandsList />
|
||||
<BrandsList openBrandDetail={openBrandDetail} />
|
||||
<AddBrandModal show={showAddBrandModal} onHide={() => setShowAddBrandModal(false)} />
|
||||
</React.Fragment>
|
||||
);
|
||||
@ -45,7 +63,7 @@ function AddBrandModal({ show, onHide }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show={show} onHide={onHide}>
|
||||
<Modal show={show} onHide={onHide} backdropClassName='modal-backdrop' container={document.body}>
|
||||
<Modal.Header>
|
||||
<Modal.Title>Add Brand</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
121
src/pages/BrandsDetail.jsx
Normal file
121
src/pages/BrandsDetail.jsx
Normal file
@ -0,0 +1,121 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import SearchBar from '../components/SearchBar';
|
||||
import { Button } 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 { findBrandById } from '../store/slices/brandsSlice';
|
||||
|
||||
export default function BrandsDetail() {
|
||||
const { id } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const [activeTab, setActiveTab] = useState('campaigns');
|
||||
const { selectedBrand } = useSelector((state) => state.brands);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
console.log(id);
|
||||
|
||||
dispatch(findBrandById(id));
|
||||
}
|
||||
}, [dispatch, id]);
|
||||
|
||||
return (
|
||||
selectedBrand?.id && (
|
||||
<React.Fragment>
|
||||
<div className='function-bar'>
|
||||
<SearchBar />
|
||||
{activeTab === 'campaigns' && (
|
||||
<Button>
|
||||
<Plus />
|
||||
Add Campaign
|
||||
</Button>
|
||||
)}
|
||||
{activeTab === 'products' && (
|
||||
<Button>
|
||||
<Plus />
|
||||
Add Product
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className='breadcrumb'>
|
||||
<Link to={'/brands'} className='breadcrumb-item'>
|
||||
Brands
|
||||
</Link>
|
||||
<div className='breadcrumb-item'>{selectedBrand.name}</div>
|
||||
</div>
|
||||
<div className='brand-detail-info'>
|
||||
<div className='brand-logo shadow-xs'>{selectedBrand.name.toUpperCase()}</div>
|
||||
<div className='brand-info shadow-xs'>
|
||||
<div className='brand-info-top'>
|
||||
<div className='info-item'>
|
||||
<div className='info-name'>
|
||||
<Folders size={20} />
|
||||
商家分类
|
||||
</div>
|
||||
<div className='info-value'>{selectedBrand.category}</div>
|
||||
</div>
|
||||
<div className='info-item'>
|
||||
<div className='info-name'>
|
||||
<LinkIcon size={20} />
|
||||
Source
|
||||
</div>
|
||||
<div className='info-value'>{selectedBrand.source}</div>
|
||||
</div>
|
||||
<div className='info-item'>
|
||||
<div className='info-name'>
|
||||
<Hash size={20} />
|
||||
Collab.
|
||||
</div>
|
||||
<div className='info-value'>{selectedBrand.collab}</div>
|
||||
</div>
|
||||
<div className='info-item'>
|
||||
<div className='info-name'>
|
||||
<Users size={20} />
|
||||
Creators
|
||||
</div>
|
||||
<div className='info-value'>{selectedBrand.creators}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='brand-info-bottom'>
|
||||
<div className='info-item'>
|
||||
<div className='info-value'>{selectedBrand.collab}</div>
|
||||
<div className='info-name'>Total Collab. Creators</div>
|
||||
</div>
|
||||
<div className='info-item'>
|
||||
<div className='info-value'>{selectedBrand.collab}</div>
|
||||
<div className='info-name'>Total GMV Achieved</div>
|
||||
</div>
|
||||
<div className='info-item'>
|
||||
<div className='info-value'>{selectedBrand.collab}</div>
|
||||
<div className='info-name'>Total Views Achieved</div>
|
||||
</div>
|
||||
<div className='info-item'>
|
||||
<div className='info-value'>{selectedBrand.collab}</div>
|
||||
<div className='info-name'>Shop Overall Rating</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='brand-tab-switches tab-switches'>
|
||||
<div
|
||||
className={`tab-switch-item ${activeTab === 'campaigns' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('campaigns')}
|
||||
>
|
||||
Campaigns
|
||||
</div>
|
||||
<div
|
||||
className={`tab-switch-item ${activeTab === 'products' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('products')}
|
||||
>
|
||||
Products
|
||||
</div>
|
||||
</div>
|
||||
{activeTab === 'campaigns' && <CampaignList />}
|
||||
{activeTab === 'products' && <ProductsList />}
|
||||
</React.Fragment>
|
||||
)
|
||||
);
|
||||
}
|
109
src/pages/CampaignDetail.jsx
Normal file
109
src/pages/CampaignDetail.jsx
Normal file
@ -0,0 +1,109 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import SearchBar from '../components/SearchBar';
|
||||
import { Button, Form } from 'react-bootstrap';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { fetchBrands, findCampaignById } from '../store/slices/brandsSlice';
|
||||
import CampaignInfo from '../components/CampaignInfo';
|
||||
import { ChevronRight, Send } from 'lucide-react';
|
||||
import ProductsList from '../components/ProductsList';
|
||||
export default function CampaignDetail() {
|
||||
const { brandId, campaignId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const { brands, selectedBrand, selectedCampaign } = useSelector((state) => state.brands);
|
||||
const progressList = ['Find', 'Review', 'Confirmed', 'Draft Ready', 'Published'];
|
||||
const [progressIndex, setProgressIndex] = useState(2);
|
||||
const [activeTab, setActiveTab] = useState('products');
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchBrands());
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (brandId && campaignId) {
|
||||
dispatch(findCampaignById({ brandId, campaignId }));
|
||||
}
|
||||
}, [brandId, campaignId]);
|
||||
|
||||
return (
|
||||
selectedCampaign?.id && (
|
||||
<div className='campaign-detail'>
|
||||
<div className='function-bar'>
|
||||
<SearchBar />
|
||||
<Button>
|
||||
<Send size={18} />
|
||||
Email
|
||||
</Button>
|
||||
</div>
|
||||
<div className='breadcrumb'>
|
||||
<Link to={'/brands'} className='breadcrumb-item'>
|
||||
Brands
|
||||
</Link>
|
||||
<Link to={`/brands/${brandId}`} className='breadcrumb-item'>{selectedBrand.name}</Link>
|
||||
<div className='breadcrumb-item'>{selectedCampaign.name}</div>
|
||||
</div>
|
||||
<CampaignInfo />
|
||||
<Form className='campaign-requirements shadow-xs'>
|
||||
<Form.Group className='mb-3 additional_requirements' controlId='additional_requirements'>
|
||||
<Form.Label>Additional Requirements</Form.Label>
|
||||
<Form.Control type='text' placeholder='xxx' />
|
||||
</Form.Group>
|
||||
<Form.Group className='mb-3 creator_requirements' controlId='creator_requirements'>
|
||||
<Form.Label>Creators</Form.Label>
|
||||
<Form.Control type='number' />
|
||||
</Form.Group>
|
||||
<Button type='submit' variant='primary-subtle'>
|
||||
Match Creators
|
||||
</Button>
|
||||
</Form>
|
||||
<div className='campaign-progress shadow-xs'>
|
||||
{progressList.map((item, index) =>
|
||||
index < progressList.length - 1 ? (
|
||||
<>
|
||||
<div className={`campaign-progress-item ${progressIndex === index ? 'active' : ''}`} key={index}>
|
||||
<div className='campaign-progress-item-index'>{index + 1}</div>
|
||||
<div className='campaign-progress-item-label'>{item}</div>
|
||||
<div className='campaign-progress-item-desc'>xx Creators</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</>
|
||||
) : (
|
||||
<div className={`campaign-progress-item ${progressIndex === index ? 'active' : ''}`} key={index}>
|
||||
<div className='campaign-progress-item-index'>{index + 1}</div>
|
||||
<div className='campaign-progress-item-label'>{item}</div>
|
||||
<div className='campaign-progress-item-desc'>xx Creators</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className='campaign-tab-switches tab-switches'>
|
||||
<div
|
||||
className={`tab-switch-item ${activeTab === 'products' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('products')}
|
||||
>
|
||||
Products
|
||||
</div>
|
||||
<div
|
||||
className={`tab-switch-item ${activeTab === 'accepted_creators' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('accepted_creators')}
|
||||
>
|
||||
Accepted Creators
|
||||
</div>
|
||||
<div
|
||||
className={`tab-switch-item ${activeTab === 'matching_result' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('matching_result')}
|
||||
>
|
||||
Matching Result
|
||||
</div>
|
||||
<div
|
||||
className={`tab-switch-item ${activeTab === 'email_draft' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('email_draft')}
|
||||
>
|
||||
Email Draft
|
||||
</div>
|
||||
</div>
|
||||
{activeTab === 'products' && <ProductsList />}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
@ -3,8 +3,10 @@ import Home from '../pages/Home';
|
||||
import Database from '../pages/Database';
|
||||
import MainLayout from '../components/Layouts/MainLayout';
|
||||
import Brands from '../pages/Brands';
|
||||
import CreatorInbox from '../pages/CreatorInbox';
|
||||
import DividLayout from '../components/Layouts/DividLayout';
|
||||
import CreatorInbox from '@/pages/CreatorInbox';
|
||||
import DividLayout from '@/components/Layouts/DividLayout';
|
||||
import BrandsDetail from '@/pages/BrandsDetail';
|
||||
import CampaignDetail from '../pages/CampaignDetail';
|
||||
|
||||
// Routes configuration object
|
||||
const routes = [
|
||||
@ -49,7 +51,14 @@ const routes = [
|
||||
path: '/brands',
|
||||
element: <Brands />,
|
||||
},
|
||||
|
||||
{
|
||||
path: '/brands/:id',
|
||||
element: <BrandsDetail />,
|
||||
},
|
||||
{
|
||||
path: '/brands/:brandId/campaigns/:campaignId',
|
||||
element: <CampaignDetail />,
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
element: <Home />,
|
||||
|
@ -1,4 +1,78 @@
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const mockCampaigns = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'SUNLINK拍拍灯',
|
||||
service: '达人短视频-付费',
|
||||
creators: 10,
|
||||
creator_type: '带货类达人',
|
||||
creator_level: ['L2', 'L3'],
|
||||
category: ['家居', '生活', '数码'],
|
||||
gmv: '$4k - $10k',
|
||||
followers: 10000,
|
||||
views: '500 - 10.5k',
|
||||
budget: '$10 - $150',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'MINISO',
|
||||
service: '达人短视频-付费',
|
||||
creators: 10,
|
||||
creator_type: '带货类达人',
|
||||
creator_level: ['L2', 'L3'],
|
||||
category: ['家居', '生活', '数码'],
|
||||
gmv: '$4k - $10k',
|
||||
followers: 10000,
|
||||
views: '500 - 10.5k',
|
||||
budget: '$10 - $150',
|
||||
},
|
||||
];
|
||||
|
||||
const mockProducts = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Name',
|
||||
commission: '15%',
|
||||
openCollab: '12%',
|
||||
availableSamples: 10,
|
||||
price: '$78.90 - $118.90',
|
||||
stock: 884,
|
||||
sold: 732,
|
||||
rating: 4.2,
|
||||
reviews: 58,
|
||||
collabCreators: 40,
|
||||
tiktokShop: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Name',
|
||||
commission: '15%',
|
||||
openCollab: '12%',
|
||||
availableSamples: 10,
|
||||
price: '$78.90 - $118.90',
|
||||
stock: 884,
|
||||
sold: 732,
|
||||
rating: 4.2,
|
||||
reviews: 58,
|
||||
collabCreators: 40,
|
||||
tiktokShop: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Name',
|
||||
commission: '15%',
|
||||
openCollab: '12%',
|
||||
availableSamples: 10,
|
||||
price: '$78.90 - $118.90',
|
||||
stock: 884,
|
||||
sold: 732,
|
||||
rating: 4.2,
|
||||
reviews: 58,
|
||||
collabCreators: 40,
|
||||
tiktokShop: true,
|
||||
},
|
||||
];
|
||||
|
||||
const mockBrands = [
|
||||
{
|
||||
@ -10,6 +84,8 @@ const mockBrands = [
|
||||
source: 'TKS Official',
|
||||
description: 'Description 1',
|
||||
website: 'https://www.brand1.com',
|
||||
campaigns: mockCampaigns,
|
||||
products: mockProducts,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@ -20,12 +96,15 @@ const mockBrands = [
|
||||
source: 'TKS Official',
|
||||
description: 'Description 1',
|
||||
website: 'https://www.brand1.com',
|
||||
campaigns: mockCampaigns,
|
||||
products: mockProducts,
|
||||
},
|
||||
];
|
||||
|
||||
export const fetchBrands = createAsyncThunk('brands/fetchBrands', async () => {
|
||||
// const response = await fetch('https://api.example.com/brands');
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.log('fetchBrands');
|
||||
|
||||
return mockBrands;
|
||||
});
|
||||
@ -35,6 +114,7 @@ const initialState = {
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
selectedBrand: {},
|
||||
selectedCampaign: {},
|
||||
};
|
||||
|
||||
const brandsSlice = createSlice({
|
||||
@ -44,6 +124,18 @@ const brandsSlice = createSlice({
|
||||
selectBrand: (state, action) => {
|
||||
state.selectedBrand = action.payload;
|
||||
},
|
||||
findBrandById: (state, action) => {
|
||||
state.selectedBrand = state.brands.find((brand) => brand.id.toString() === action.payload);
|
||||
},
|
||||
findCampaignById: (state, action) => {
|
||||
const { brandId, campaignId } = action.payload;
|
||||
console.log(brandId, campaignId);
|
||||
console.log(state.brands);
|
||||
const brand = state.brands?.find((b) => b.id.toString() === brandId);
|
||||
|
||||
state.selectedBrand = brand;
|
||||
state.selectedCampaign = brand?.campaigns?.find((c) => c.id.toString() === campaignId) || {};
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
@ -61,6 +153,6 @@ const brandsSlice = createSlice({
|
||||
},
|
||||
});
|
||||
|
||||
export const { selectBrand } = brandsSlice.actions;
|
||||
export const { selectBrand, findBrandById, findCampaignById } = brandsSlice.actions;
|
||||
|
||||
export default brandsSlice.reducer;
|
||||
|
@ -58,3 +58,324 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.brand-detail-info {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
gap: 1rem;
|
||||
|
||||
.brand-logo {
|
||||
height: 12.5rem;
|
||||
max-width: 25rem;
|
||||
flex-grow: 1;
|
||||
background-color: #fff;
|
||||
color: $primary;
|
||||
font-size: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 800;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
.brand-info {
|
||||
flex-grow: 1;
|
||||
font-size: 0.875rem;
|
||||
background-color: #fff;
|
||||
border-radius: 0.375rem;
|
||||
|
||||
.brand-info-top {
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid $neutral-200;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
|
||||
.info-name {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.brand-info-bottom {
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background-color: $neutral-150;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem 0;
|
||||
flex: 1;
|
||||
|
||||
.info-value {
|
||||
font-weight: 800;
|
||||
}
|
||||
.info-name {
|
||||
font-weight: 600;
|
||||
color: $neutral-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-switches {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin: 1rem 0;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 0.375rem;
|
||||
|
||||
.tab-switch-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 0.5rem 0;
|
||||
background-color: $neutral-200;
|
||||
transition: 0.25s;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 0.375rem;
|
||||
border-bottom-left-radius: 0.375rem;
|
||||
}
|
||||
&:last-child {
|
||||
border-top-right-radius: 0.375rem;
|
||||
border-bottom-right-radius: 0.375rem;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $primary;
|
||||
color: white;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
color: white;
|
||||
background-color: $primary;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color: $neutral-350;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.campaigns-list {
|
||||
display: flex;
|
||||
gap: 0.875rem;
|
||||
|
||||
.campaign-info {
|
||||
background-color: #fff;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 2px solid transparent;
|
||||
transition: 0.25s;
|
||||
width: 350px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
gap: 0.5rem;
|
||||
// &:hover {
|
||||
// border: 2px solid $violet-400;
|
||||
// }
|
||||
|
||||
.campaign-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: .5rem;
|
||||
color: $primary;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.campaign-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.campaign-item-label {
|
||||
width: 120px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.campaign-item-value {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.creator-level-tag {
|
||||
background-color: #e8f0fe;
|
||||
color: #1967d2;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.category-tag {
|
||||
color: $primary;
|
||||
padding: 2px 8px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
border: 1px solid $primary;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.campaign-detail {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
gap: 1rem;
|
||||
|
||||
.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;
|
||||
right: 0;
|
||||
top: 0;
|
||||
color: $primary;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.campaign-info-bottom {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
|
||||
.campaign-info-item {
|
||||
width: 30%;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
|
||||
.campaign-info-item-label {
|
||||
color: $neutral-700;
|
||||
width: 8.5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.campaign-requirements {
|
||||
width: 30%;
|
||||
max-width: 380px;
|
||||
background-color: #fff;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 0.375rem;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
.additional_requirements {
|
||||
width: 100%;
|
||||
}
|
||||
.creator_requirements {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 50%;
|
||||
.form-label {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.campaign-progress {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 0.375rem;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.campaign-progress-item {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
flex: 1;
|
||||
border-bottom: 4px solid transparent;
|
||||
padding: .375rem 0;
|
||||
|
||||
.campaign-progress-item-index {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
text-align: center;
|
||||
line-height: 1.75rem;
|
||||
border-radius: 50%;
|
||||
border: 1px solid $neutral-600;
|
||||
color: $neutral-700;
|
||||
}
|
||||
.campaign-progress-item-desc {
|
||||
font-size: 0.75rem;
|
||||
color: $neutral-600;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: $primary;
|
||||
.campaign-progress-item-index {
|
||||
border-color: $primary;
|
||||
background-color: $primary;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,186 +1,131 @@
|
||||
@import './custom-theme.scss';
|
||||
|
||||
.creator-database-table {
|
||||
.table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
.creator-cell {
|
||||
.creator-avatar {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-right: 12px;
|
||||
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
border-top: none;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1rem 0.75rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: #495057;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f1f3f5;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.75rem;
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(99, 102, 241, 0.05);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
.verified-badge {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: #10b981;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
font-size: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
.creator-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.creator-cell {
|
||||
.creator-avatar {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-right: 12px;
|
||||
.category-pill {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
font-size: 0.75rem;
|
||||
color: #495057;
|
||||
white-space: nowrap;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
&.phones {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
&.women {
|
||||
background-color: rgba(244, 114, 182, 0.1);
|
||||
color: #f472b6;
|
||||
}
|
||||
|
||||
&.sports {
|
||||
background-color: rgba(52, 211, 153, 0.1);
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
&.food {
|
||||
background-color: rgba(251, 146, 60, 0.1);
|
||||
color: #fb923c;
|
||||
}
|
||||
|
||||
&.health {
|
||||
background-color: rgba(56, 189, 248, 0.1);
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
&.kitchen {
|
||||
background-color: rgba(168, 85, 247, 0.1);
|
||||
color: #a855f7;
|
||||
}
|
||||
}
|
||||
|
||||
.level-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&.ecommerce-level {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
&.exposure-level {
|
||||
background-color: rgba(14, 165, 233, 0.1);
|
||||
color: #0ea5e9;
|
||||
|
||||
&[data-level^='KOC'] {
|
||||
background-color: rgba(52, 211, 153, 0.1);
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
&[data-level^='KOL'] {
|
||||
background-color: rgba(251, 113, 133, 0.1);
|
||||
color: #fb7185;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.colored-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 2px solid #e2e8f0;
|
||||
}
|
||||
display: inline-block;
|
||||
|
||||
.verified-badge {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: #10b981;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
font-size: 8px;
|
||||
&.blue {
|
||||
background-color: #6366f1;
|
||||
}
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.tiktok-icon {
|
||||
font-size: 14px;
|
||||
color: #000000;
|
||||
}
|
||||
}
|
||||
|
||||
.creator-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.category-pill {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
font-size: 0.75rem;
|
||||
color: #495057;
|
||||
white-space: nowrap;
|
||||
|
||||
&.phones {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
&.women {
|
||||
background-color: rgba(244, 114, 182, 0.1);
|
||||
color: #f472b6;
|
||||
}
|
||||
|
||||
&.sports {
|
||||
background-color: rgba(52, 211, 153, 0.1);
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
&.food {
|
||||
background-color: rgba(251, 146, 60, 0.1);
|
||||
color: #fb923c;
|
||||
}
|
||||
|
||||
&.health {
|
||||
background-color: rgba(56, 189, 248, 0.1);
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
&.kitchen {
|
||||
background-color: rgba(168, 85, 247, 0.1);
|
||||
color: #a855f7;
|
||||
}
|
||||
}
|
||||
|
||||
.level-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&.ecommerce-level {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
&.exposure-level {
|
||||
background-color: rgba(14, 165, 233, 0.1);
|
||||
color: #0ea5e9;
|
||||
|
||||
&[data-level^="KOC"] {
|
||||
background-color: rgba(52, 211, 153, 0.1);
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
&[data-level^="KOL"] {
|
||||
background-color: rgba(251, 113, 133, 0.1);
|
||||
color: #fb7185;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.colored-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
|
||||
&.blue {
|
||||
background-color: #6366f1;
|
||||
}
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.tiktok-icon {
|
||||
font-size: 14px;
|
||||
color: #000000;
|
||||
}
|
||||
}
|
||||
}
|
16
src/styles/Products.scss
Normal file
16
src/styles/Products.scss
Normal file
@ -0,0 +1,16 @@
|
||||
.products-list {
|
||||
width: 100%;
|
||||
|
||||
.product-logo {
|
||||
position: relative;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
line-height: 3rem;
|
||||
text-align: center;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #7f55e0;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
margin-right: .25rem;
|
||||
}
|
||||
}
|
@ -15,10 +15,11 @@ $indigo-100: #e0e7ff;
|
||||
$indigo-500: #6366f1;
|
||||
$violet-50: #f5f3ff;
|
||||
$violet-100: #ede9fe;
|
||||
$violet-150: #E0E1FAFF;
|
||||
$neutral-150: #f8f9faFF;
|
||||
$neutral-200: #F3F4F6FF;
|
||||
$neutral-350: #CFD2DAFF;
|
||||
$violet-150: #e0e1faff;
|
||||
$violet-400: #a78bfa;
|
||||
$neutral-150: #f8f9faff;
|
||||
$neutral-200: #f3f4f6ff;
|
||||
$neutral-350: #cfd2daff;
|
||||
$neutral-600: #565e6cff;
|
||||
$neutral-700: #323842ff;
|
||||
$neutral-900: #171a1fff;
|
||||
@ -37,7 +38,7 @@ $box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
|
||||
:root {
|
||||
--bs-breadcrumb-font-size: 1.5rem;
|
||||
--bs-body-color: #171A1FFF;
|
||||
--bs-body-color: #171a1fff;
|
||||
--bs-btn-color: white !important;
|
||||
--bs-btn-hover-color: white !important;
|
||||
}
|
||||
@ -51,7 +52,17 @@ $box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-btn-hover-color: white !important;
|
||||
}
|
||||
|
||||
.btn-primary-subtle {
|
||||
background-color: $violet-150;
|
||||
color: $indigo-500;
|
||||
&:hover {
|
||||
background-color: $primary !important;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
#root {
|
||||
font-weight: 500;
|
||||
background-color: #f5f3ff;
|
||||
}
|
||||
|
||||
@ -61,3 +72,62 @@ a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.shadow-xs {
|
||||
box-shadow: 0px 0px 1px #171a1f12, 0px 0px 2px #171a1f1f; /* shadow-xs */
|
||||
}
|
||||
|
||||
.table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
border-top: none;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1rem 0.75rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: #495057;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f1f3f5;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.75rem;
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(99, 102, 241, 0.05);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,13 @@
|
||||
.function-bar {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
float: right;
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user