[dev]product basic info

This commit is contained in:
susie-laptop 2025-05-24 11:15:54 -04:00
parent 711c4652bb
commit 0fa465a2c6
10 changed files with 164 additions and 135 deletions

View File

@ -25,7 +25,7 @@ export default function CampaignList() {
<div className='campaigns-list'> <div className='campaigns-list'>
{selectedBrand?.campaigns?.length > 0 && {selectedBrand?.campaigns?.length > 0 &&
selectedBrand.campaigns.map((campaign) => ( selectedBrand.campaigns.map((campaign) => (
<div className='campaign-info'> <div className='campaign-info' key={campaign.id}>
<Link to={`/brands/${selectedBrand.id}/campaigns/${campaign.id}`} className='campaign-title'> <Link to={`/brands/${selectedBrand.id}/campaigns/${campaign.id}`} className='campaign-title'>
{campaign.name} {campaign.name}
</Link> </Link>

View File

@ -0,0 +1,55 @@
import { useSelector } from "react-redux";
export default function ProductDetail() {
const selectedProduct = useSelector((state) => state.brands.selectedProduct);
return (
<div className='product-details shadow-xs'>
<div className='product-details-header'>
<div className='product-details-header-title'>{selectedProduct.name}</div>
<div className='product-details-header-pid'>PID: {selectedProduct.id}</div>
</div>
<div className='product-details-body'>
<div className='product-img'></div>
<div className='product-detail-list'>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.commission_rate}</div>
<div className='product-detail-item-label'>Commission Rate</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.available_samples}</div>
<div className='product-detail-item-label'>Available Samples</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.sales_price_max} - {selectedProduct.sales_price_min}</div>
<div className='product-detail-item-label'>Sales Price</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.stock}</div>
<div className='product-detail-item-label'>Stock</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.items_sold}</div>
<div className='product-detail-item-label'>Items Sold</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.product_rating}</div>
<div className='product-detail-item-label'>Product Rating</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.collab_creators}</div>
<div className='product-detail-item-label'>Collab Creators</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.gmv}</div>
<div className='product-detail-item-label'>GMV Achieved</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.reviews_count}</div>
<div className='product-detail-item-label'>Views Achieved</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -16,6 +16,7 @@ export default function ProductsList({ onShowProductDetail }) {
const handleSort = (field) => { const handleSort = (field) => {
return;
if (sortField === field) { if (sortField === field) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else { } else {
@ -25,6 +26,7 @@ export default function ProductsList({ onShowProductDetail }) {
}; };
const renderSortIcon = (field) => { const renderSortIcon = (field) => {
return;
if (sortField !== field) return null; if (sortField !== field) return null;
return sortDirection === 'asc' ? '↑' : '↓'; return sortDirection === 'asc' ? '↑' : '↓';
}; };
@ -109,7 +111,7 @@ 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.id)} 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> <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>

View File

@ -6,13 +6,16 @@ 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 } from '../store/slices/brandsSlice'; import { fetchBrandDetail, fetchBrandCampaigns, fetchBrandProducts, setSelectedProduct } from '../store/slices/brandsSlice';
import SlidePanel from '../components/SlidePanel';
import ProductDetail from '../components/ProductDetail';
export default function BrandsDetail() { export default function BrandsDetail() {
const { id } = useParams(); const { id } = useParams();
const dispatch = useDispatch(); const dispatch = useDispatch();
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);
useEffect(() => { useEffect(() => {
if (id) { if (id) {
@ -22,6 +25,12 @@ export default function BrandsDetail() {
} }
}, [dispatch, id]); }, [dispatch, id]);
const handleShowProductDetail = (product) => {
dispatch(setSelectedProduct(product));
setShowProductDetail(true);
};
return ( return (
selectedBrand?.id && ( selectedBrand?.id && (
<React.Fragment> <React.Fragment>
@ -114,7 +123,19 @@ export default function BrandsDetail() {
</div> </div>
</div> </div>
{activeTab === 'campaigns' && <CampaignList />} {activeTab === 'campaigns' && <CampaignList />}
{activeTab === 'products' && <ProductsList onShowProductDetail={() => {}} />} {activeTab === 'products' && (
<>
<ProductsList onShowProductDetail={handleShowProductDetail} />
<SlidePanel
show={showProductDetail}
onClose={() => setShowProductDetail(false)}
title='Product Detail'
size='xxl'
>
<ProductDetail />
</SlidePanel>
</>
)}
</React.Fragment> </React.Fragment>
) )
); );

View File

@ -3,7 +3,7 @@ import { Link, useParams } from 'react-router-dom';
import SearchBar from '../components/SearchBar'; import SearchBar from '../components/SearchBar';
import { Button, Form, Modal } from 'react-bootstrap'; import { Button, Form, Modal } from 'react-bootstrap';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { fetchBrands, fetchBrandDetail, findProductById, fetchCampaignDetail } from '../store/slices/brandsSlice'; import { fetchBrands, fetchBrandDetail, fetchCampaignDetail, setSelectedProduct } from '../store/slices/brandsSlice';
import CampaignInfo from '../components/CampaignInfo'; import CampaignInfo from '../components/CampaignInfo';
import { ChevronRight, Send, Plus } from 'lucide-react'; import { ChevronRight, Send, Plus } from 'lucide-react';
import ProductsList from '../components/ProductsList'; import ProductsList from '../components/ProductsList';
@ -20,36 +20,25 @@ export default function CampaignDetail() {
const [showProductDetail, setShowProductDetail] = useState(false); const [showProductDetail, setShowProductDetail] = useState(false);
const [showAddProductModal, setShowAddProductModal] = useState(false); const [showAddProductModal, setShowAddProductModal] = useState(false);
useEffect(() => {
dispatch(fetchBrands());
}, [dispatch]);
useEffect(() => { useEffect(() => {
if (brandId && campaignId) { if (brandId && campaignId) {
if (!selectedBrand) { if (!selectedBrand?.id) {
dispatch(fetchBrandDetail(brandId)); dispatch(fetchBrandDetail(brandId));
dispatch(fetchCampaignDetail(campaignId)); dispatch(fetchCampaignDetail(campaignId));
} else { } else {
dispatch(fetchCampaignDetail(campaignId)); dispatch(fetchCampaignDetail(campaignId));
} }
} }
}, [brandId, campaignId]); }, [dispatch, brandId, campaignId]);
const handleShowProductDetail = (productId) => { const handleShowProductDetail = (product) => {
dispatch(findProductById({ brandId, campaignId, productId })); dispatch(setSelectedProduct(product));
setShowProductDetail(true); setShowProductDetail(true);
}; };
return ( return (
selectedCampaign?.id && ( selectedCampaign?.id && (
<div className='campaign-detail'> <div className='campaign-detail'>
<div className='function-bar'>
<SearchBar />
<Button>
<Send size={18} />
Email
</Button>
</div>
<div className='breadcrumb'> <div className='breadcrumb'>
<Link to={'/brands'} className='breadcrumb-item'> <Link to={'/brands'} className='breadcrumb-item'>
Brands Brands
@ -59,6 +48,13 @@ export default function CampaignDetail() {
</Link> </Link>
<div className='breadcrumb-item'>{selectedCampaign.name}</div> <div className='breadcrumb-item'>{selectedCampaign.name}</div>
</div> </div>
<div className='function-bar'>
<SearchBar />
<Button>
<Send size={18} />
Email
</Button>
</div>
<CampaignInfo /> <CampaignInfo />
<Form className='campaign-requirements shadow-xs'> <Form className='campaign-requirements shadow-xs'>
<Form.Group className='mb-3 additional_requirements' controlId='additional_requirements'> <Form.Group className='mb-3 additional_requirements' controlId='additional_requirements'>

View File

@ -4,6 +4,7 @@ import { Accordion, Button, Card, Col, Form, Row, Table } from 'react-bootstrap'
import { CloudUpload, Paperclip } from 'lucide-react'; import { CloudUpload, Paperclip } from 'lucide-react';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import { mockCreators } from '../store/slices/creatorsSlice'; import { mockCreators } from '../store/slices/creatorsSlice';
import ProductDetail from '../components/ProductDetail';
export default function CampaignScript() { export default function CampaignScript() {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -17,53 +18,7 @@ export default function CampaignScript() {
return ( return (
selectedProduct?.id && ( selectedProduct?.id && (
<div className='product-script'> <div className='product-script'>
<div className='product-details shadow-xs'> <ProductDetail />
<div className='product-details-header'>
<div className='product-details-header-title'>{selectedProduct.name}</div>
<div className='product-details-header-pid'>PID: {selectedProduct.id}</div>
</div>
<div className='product-details-body'>
<div className='product-img'></div>
<div className='product-detail-list'>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.commission}</div>
<div className='product-detail-item-label'>Commission Rate</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.availableSamples}</div>
<div className='product-detail-item-label'>Available Samples</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.price}</div>
<div className='product-detail-item-label'>Sales Price</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.stock}</div>
<div className='product-detail-item-label'>Stock</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.sold}</div>
<div className='product-detail-item-label'>Items Sold</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.rating}</div>
<div className='product-detail-item-label'>Product Rating</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.collabCreators}</div>
<div className='product-detail-item-label'>Collab Creators</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.gmv}</div>
<div className='product-detail-item-label'>GMV Achieved</div>
</div>
<div className='product-detail-item'>
<div className='product-detail-item-value'>{selectedProduct.reviews}</div>
<div className='product-detail-item-label'>Views Achieved</div>
</div>
</div>
</div>
</div>
<div className='product-script-switches tab-switches'> <div className='product-script-switches tab-switches'>
<div <div
className={`tab-switch-item ${activeTab === 'collaborationInfo' ? 'active' : ''}`} className={`tab-switch-item ${activeTab === 'collaborationInfo' ? 'active' : ''}`}

View File

@ -218,13 +218,8 @@ const brandsSlice = createSlice({
state.selectedBrand = brand; state.selectedBrand = brand;
state.selectedCampaign = brand?.campaigns?.find((c) => c.id.toString() === campaignId) || {}; state.selectedCampaign = brand?.campaigns?.find((c) => c.id.toString() === campaignId) || {};
}, },
findProductById: (state, action) => { setSelectedProduct: (state, action) => {
const { brandId, campaignId, productId } = action.payload; state.selectedProduct = action.payload;
const brand = state.brands?.find((b) => b.id.toString() === brandId);
const campaign = brand?.campaigns?.find((c) => c.id.toString() === campaignId);
const product = campaign?.products?.find((p) => p.id.toString() === productId.toString());
console.log(brand, campaign, product);
state.selectedProduct = product;
}, },
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
@ -308,6 +303,6 @@ const brandsSlice = createSlice({
}, },
}); });
export const { selectBrand, findBrandById, findCampaignById, findProductById } = brandsSlice.actions; export const { selectBrand, findBrandById, findCampaignById, setSelectedProduct } = brandsSlice.actions;
export default brandsSlice.reducer; export default brandsSlice.reducer;

View File

@ -267,6 +267,9 @@
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
gap: 1rem; gap: 1rem;
justify-content: space-between;
height: 100%;
overflow-y: auto;
.campaign-detail-info { .campaign-detail-info {
width: 100%; width: 100%;

View File

@ -7,65 +7,6 @@
flex-flow: column nowrap; flex-flow: column nowrap;
gap: 1rem; gap: 1rem;
.product-details {
background-color: #fff;
padding: 1.5rem;
border-radius: 0.5rem;
.product-details-header {
display: flex;
flex-flow: column nowrap;
align-items: flex-start;
.product-details-header-title {
font-size: 1rem;
color: $primary;
font-weight: 800;
}
.product-details-header-pid {
font-size: 0.75rem;
color: $neutral-600;
}
}
.product-details-body {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
.product-img {
flex-shrink: 0;
width: 16rem;
padding-right: 1rem;
height: 15rem;
background-color: $neutral-200;
border-radius: 0.5rem;
}
.product-detail-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
justify-content: space-between;
gap: 0.5rem;
.product-detail-item {
display: flex;
flex-flow: column nowrap;
align-items: center;
background-color: $neutral-150;
border-radius: 0.375rem;
padding: 0.75rem 0;
width: 10rem;
.product-detail-item-value {
font-size: 1rem;
font-weight: 600;
}
.product-detail-item-label {
font-size: 0.875rem;
color: $neutral-600;
}
}
}
}
}
.product-script-video-req { .product-script-video-req {
.video-req-form { .video-req-form {
display: flex; display: flex;

View File

@ -17,3 +17,64 @@
margin-right: .25rem; margin-right: .25rem;
} }
} }
.product-details {
background-color: #fff;
padding: 1.5rem;
border-radius: 0.5rem;
.product-details-header {
display: flex;
flex-flow: column nowrap;
align-items: flex-start;
.product-details-header-title {
font-size: 1rem;
color: $primary;
font-weight: 800;
}
.product-details-header-pid {
font-size: 0.75rem;
color: $neutral-600;
}
}
.product-details-body {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
.product-img {
flex-shrink: 0;
width: 16rem;
padding-right: 1rem;
height: 15rem;
background-color: $neutral-200;
border-radius: 0.5rem;
}
.product-detail-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
justify-content: space-between;
gap: 0.5rem;
.product-detail-item {
display: flex;
flex-flow: column nowrap;
align-items: center;
background-color: $neutral-150;
border-radius: 0.375rem;
padding: 0.75rem 0;
width: 10rem;
.product-detail-item-value {
font-size: 1rem;
font-weight: 600;
}
.product-detail-item-label {
font-size: 0.875rem;
color: $neutral-600;
}
}
}
}
}