[dev]campaign tabs

This commit is contained in:
susie-laptop 2025-05-28 21:28:25 -04:00
parent 3492e16c40
commit 45d7a7de1a
3 changed files with 428 additions and 45 deletions

View File

@ -114,40 +114,32 @@ export const CREATOR_LEVELS = [
];
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',
},
{ value: 'Phones & Electronics', name: 'Phones & Electronics' },
{ value: 'Homes Supplies', name: 'Homes Supplies' },
{ value: 'Health', name: 'Health' },
{ value: 'Textiles & Soft Furnishings', name: 'Textiles & Soft Furnishings' },
{ value: 'Household Appliances', name: 'Household Appliances' },
{ value: 'Womenswear & Underwear', name: 'Womenswear & Underwear' },
{ value: 'Muslim Fashion', name: 'Muslim Fashion' },
{ value: 'Shoes', name: 'Shoes' },
{ value: 'Beauty & Personal Care', name: 'Beauty & Personal Care' },
{ value: 'Pet Supplies', name: 'Pet Supplies' },
{ value: 'Computers & Office Equipment', name: 'Computers & Office Equipment' },
{ value: 'Baby & Maternity', name: 'Baby & Maternity' },
{ value: 'Sports & Outdoor', name: 'Sports & Outdoor' },
{ value: 'Toys', name: 'Toys' },
{ value: 'Furniture', name: 'Furniture' },
{ value: 'Kitchenware', name: 'Kitchenware' },
{ value: 'Home Improvement', name: 'Home Improvement' },
{ value: 'Tools & Hardware', name: 'Tools & Hardware' },
{ value: "Kids' Fashion", name: "Kids' Fashion" },
{ value: 'Automotive & Motorcycle', name: 'Automotive & Motorcycle' },
{ value: 'Fashion Accessories', name: 'Fashion Accessories' },
{ value: 'Food & Beverages', name: 'Food & Beverages' },
{ value: 'Books, Magazines & Audio', name: 'Books, Magazines & Audio' },
{ value: 'Menswear & Underwear', name: 'Menswear & Underwear' },
{ value: 'Pre-Owned Collections', name: 'Pre-Owned Collections' },
{ value: 'Luggage & Bags', name: 'Luggage & Bags' },
{ value: 'Jewellery Accessories & Derivatives', name: 'Jewellery Accessories & Derivatives' },
{ value: 'Other', name: 'Other' },
];

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import SearchBar from '../components/SearchBar';
import { Button, Form, Modal } from 'react-bootstrap';
import { Button, Col, Form, Modal, Row, Spinner, Table } from 'react-bootstrap';
import { useSelector, useDispatch } from 'react-redux';
import { fetchBrands, fetchBrandDetail, fetchCampaignDetail, setSelectedProduct } from '../store/slices/brandsSlice';
import CampaignInfo from '../components/CampaignInfo';
@ -134,18 +134,27 @@ export default function CampaignDetail() {
<Plus size={18} />
Add Product
</Button>
<ProductsList products={selectedCampaign?.link_product_details} onShowProductDetail={handleShowProductDetail} />
<ProductsList
products={selectedCampaign?.link_product_details}
onShowProductDetail={handleShowProductDetail}
/>
<SlidePanel
show={showProductDetail}
onClose={() => setShowProductDetail(false)}
title='Product Detail'
size='xxl'
>
<CampaignScript />
<CampaignScript />
</SlidePanel>
<AddProductModal campaignId={campaignId} show={showAddProductModal} onHide={() => setShowAddProductModal(false)} />
<AddProductModal
campaignId={campaignId}
show={showAddProductModal}
onHide={() => setShowAddProductModal(false)}
/>
</>
)}
{activeTab === 'accepted_creators' && <AcceptedCreators />}
{activeTab === 'matching_result' && <MatchingResult />}
</div>
)
);
@ -184,11 +193,12 @@ function AddProductModal({ campaignId, show, onHide }) {
onChange={(e) => setSelectedProduct(e.target.value)}
>
<option>Select</option>
{products?.length > 0 && products.map((product) => (
<option key={product.id} value={product.id}>
{product.name}
</option>
))}
{products?.length > 0 &&
products.map((product) => (
<option key={product.id} value={product.id}>
{product.name}
</option>
))}
</Form.Select>
</Form.Group>
<div className='button-group'>
@ -204,3 +214,320 @@ function AddProductModal({ campaignId, show, onHide }) {
</Modal>
);
}
function AcceptedCreators() {
const { selectedCampaign } = useSelector((state) => state.brands);
const handleSort = (field) => {
return;
console.log(field);
};
const renderSortIcon = (field) => {
return;
return <ChevronDown size={18} />;
};
const renderStatusPill = (status) => {
switch (status) {
case 'unconnected':
return <span className='status-pill'>Unconnected</span>;
case 'connected':
return <span className='status-pill'>Connected</span>;
case 'pending':
return <span className='status-pill'>Pending</span>;
case 'rejected':
return <span className='status-pill'>Rejected</span>;
case 'accepted':
return <span className='status-pill'>Accepted</span>;
case 'provided':
return <span className='status-pill'>Provided</span>;
case 'completed':
return <span className='status-pill'>Completed</span>;
case 'draft':
return <span className='status-pill'>Draft</span>;
case 'published':
return <span className='status-pill'>Published</span>;
default:
return <span className='status-pill'>Unknown</span>;
}
};
return (
<div className='accepted-creators-table w-100'>
<div className='table-container'>
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden'>
<thead className='sticky-header'>
<tr>
{/* <th className='selector' style={{ width: '40px' }}>
<Form.Check
type='checkbox'
checked={
selectedCampaign?.creators?.length === publicCreators.length && publicCreators.length > 0
}
onChange={handleSelectAll}
/>
</th> */}
<th className='creator' onClick={() => handleSort('name')} style={{ width: '180px' }}>
Creator {renderSortIcon('name')}
</th>
<th
className='category text-center'
onClick={() => handleSort('category')}
style={{ width: '180px' }}
>
Category {renderSortIcon('category')}
</th>
<th className='followers text-center' onClick={() => handleSort('followers')}>
Followers {renderSortIcon('followers')}
</th>
<th className='gmv text-center' onClick={() => handleSort('gmv')}>
GMV {renderSortIcon('gmv')}
</th>
<th className='views text-center' onClick={() => handleSort('avg_video_views')}>
Avg. Video Views {renderSortIcon('avg_video_views')}
</th>
<th className='status text-center'>Status</th>
<th className='pricing text-center'>Pricing</th>
<th className='collaboration text-center'>Collaboration</th>
<th className='profile text-center'>Profile</th>
</tr>
</thead>
<tbody>
{!selectedCampaign?.creators || selectedCampaign?.creators?.length <= 0 ? (
<>
<tr>
<td colSpan='9' className='text-center py-4'>
No creators found.
</td>
</tr>
<tr>
<td colSpan={9}>
<StepProgress />
</td>
</tr>
</>
) : (
selectedCampaign?.creators?.map((creator) => (
<tr
key={creator.creator_id}
className={
selectedCampaign?.creators?.includes(creator.creator_id) ? 'selected' : ''
}
>
{/* <td>
<Form.Check
type='checkbox'
checked={selectedCreators.includes(creator.creator_id)}
onChange={() => handleSelectCreator(creator.creator_id)}
/>
</td> */}
<td className='creator-cell'>
<div className='d-flex align-items-center'>
<div className='creator-avatar'>
<img src={creator.avatar} alt={creator.name} />
{creator.status && <span className='verified-badge'></span>}
</div>
<Link to={`/creator/${creator.creator_id}`} className='creator-name'>
{creator.name}
</Link>
</div>
</td>
<td>
<span className={`category-pill ${getCategoryClassName(creator.category)}`}>
{creator.category}
</span>
</td>
<td className='text-nowrap text-center'>{creator.followers}</td>
<td className='text-center'>
<div>{creator.gmv}</div>
<div className='small text-muted'>Items Sold: {creator.soldPercentage}</div>
</td>
<td className='text-nowrap text-center'>{creator.avg_video_views}</td>
<td className='text-center'>
<div className='status-pill'>{renderStatusPill(creator.status)}</div>
</td>
<td className='text-center'>{creator.pricing}</td>
<td className='text-center'>{creator.collaboration}</td>
<td className='text-center'>{creator.profile}</td>
<td colSpan={8}>
<StepProgress dates={creator.dates} />
</td>
</tr>
))
)}
</tbody>
</Table>
</div>
</div>
);
}
function StepProgress({ dates = [] }) {
const labels = [
'Brand Review',
'Price Negotiation',
'Creator Confirmation',
'Draft Ready',
'Draft Approved',
'Published',
];
const steps = [
{ label: 'Brand Review', date: '12/30/2020' },
{ label: 'Price Negotiation', date: '12/30/2020' },
{ label: 'Creator Confirmation', date: '12/30/2020' },
{ label: 'Draft Ready', date: '12/30/2020' },
{ label: 'Draft Approved', date: '12/30/2020' },
{ label: 'Published', date: '12/30/2020' },
];
return (
<div className='inline-step-progress'>
<div className='inline-step'></div>
{steps.map((step, index) => {
const isCompleted = !!dates[index];
return (
<div className='inline-step' key={index}>
<div className={`circle ${isCompleted ? 'completed' : 'pending'}`}>
{isCompleted ? '✔' : ''}1
</div>
{index !== steps.length - 1 && <div className='line' />}
<div className='label'>
<span className='label-text'>{step.label}</span>
<br />
<span className='label-date'>{step.date || 'N/A'}</span>
</div>
</div>
);
})}
</div>
);
}
function MatchingResult() {
const [showMatchingResultModal, setShowMatchingResultModal] = useState(false);
const mockData = [
{
operation: 'Operation 1',
creator_find: 100,
creator_accepted: 100,
avg_creator_pricing: 100,
date: '2020-01-01',
},
];
return (
<div className='matching-result-table w-100'>
<div className='table-container'>
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden'>
<thead className='sticky-header'>
<tr>
<th># Operation</th>
<th># Creator Find</th>
<th># Creator Accepted</th>
<th>Avg. Creator Pricing</th>
<th>Date</th>
<th>View Details</th>
</tr>
</thead>
<tbody>
{mockData.length > 0 ? (
mockData.map((item, index) => (
<tr key={index}>
<td>{item.operation}</td>
<td>{item.creator_find}</td>
<td>{item.creator_accepted}</td>
<td>{item.avg_creator_pricing}</td>
<td>{item.date}</td>
<td>
<Button
variant='outline-primary'
className='border-0'
onClick={() => setShowMatchingResultModal(true)}
>
View
</Button>
</td>
</tr>
))
) : (
<tr>
<td colSpan={6} className='text-center'>
No data
</td>
</tr>
)}
</tbody>
</Table>
<CampaignMatchingResult
show={showMatchingResultModal}
onHide={() => setShowMatchingResultModal(false)}
/>
</div>
</div>
);
}
function CampaignMatchingResult({ show, onHide }) {
const mockData = [
{
creator: 'Creator 1',
category: 'Category 1',
followers: 100,
gmv: 100,
avg_video_views: 100,
status: 'Status 1',
pricing: 100,
profile: 'Profile 1',
},
];
return (
<Modal show={show} onHide={onHide} size='lg'>
<Modal.Header closeButton className='fw-bold'>
Campaign Matching Result
</Modal.Header>
<Modal.Body>
<div className='matching-result-table'>
<div className='table-container'>
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden'>
<thead className='sticky-header'>
<tr>
<th>Creator</th>
<th>Category</th>
<th>Followers</th>
<th>GMV</th>
<th>Avg. Video Views</th>
<th>Status</th>
<th>Pricing</th>
<th>Profile</th>
</tr>
</thead>
<tbody>
{mockData.length > 0 ? (
mockData.map((item, index) => (
<tr key={index}>
<td>{item.creator}</td>
<td>{item.category}</td>
<td>{item.followers}</td>
<td>{item.gmv}</td>
<td>{item.avg_video_views}</td>
<td>{item.status}</td>
<td>{item.pricing}</td>
<td>{item.profile}</td>
</tr>
))
) : (
<tr>
<td colSpan={6} className='text-center'>
No data
</td>
</tr>
)}
</tbody>
</Table>
</div>
</div>
</Modal.Body>
</Modal>
);
}

View File

@ -47,3 +47,67 @@
}
}
}
.inline-step-progress {
display: flex;
align-items: center;
position: relative;
width: 100%;
justify-content: space-between;
padding: 16px 0 65px 0;
.inline-step {
position: relative;
display: flex;
align-items: center;
width: 100%;
}
.circle {
width: 14px;
height: 14px;
border-radius: 50%;
font-size: 10px;
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
}
.completed {
background-color: #6f6ee3;
color: white;
}
.pending {
background-color: #fff;
border: 1px solid #ccc;
color: transparent;
}
.line {
height: 2px;
width: 100%;
background-color: #ccc;
z-index: 0;
}
.label {
position: absolute;
top: 18px;
text-align: center;
left: -45%;
width: 100%;
.label-text {
display: inline-block;
}
.label-date {
display: inline-block;
}
}
}
.matching-result-table {
th, td {
text-align: center;
}
}