Compare commits

...

3 Commits

Author SHA1 Message Date
a248d7dedf [dev]productdetail 2025-05-26 12:55:28 -04:00
855ea29b92 [dev]add prod to campaign
from band page
2025-05-26 12:26:10 -04:00
10f3420890 [dev]add product 2025-05-26 12:11:13 -04:00
11 changed files with 426 additions and 107 deletions

View File

@ -1,7 +1,16 @@
import { useSelector } from "react-redux"; import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchProductDetail } from '../store/slices/productSlice';
import { mockCreators } from '../store/slices/creatorsSlice';
import { Accordion, Table } from 'react-bootstrap';
export default function ProductDetail() { export default function ProductDetail() {
const selectedProduct = useSelector((state) => state.brands.selectedProduct); const selectedProduct = useSelector((state) => state.brands.selectedProduct);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchProductDetail(selectedProduct.id));
}, [selectedProduct.id]);
return ( return (
<div className='product-details shadow-xs'> <div className='product-details shadow-xs'>
@ -21,7 +30,9 @@ export default function ProductDetail() {
<div className='product-detail-item-label'>Available Samples</div> <div className='product-detail-item-label'>Available Samples</div>
</div> </div>
<div className='product-detail-item'> <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-value'>
{selectedProduct.sales_price_max} - {selectedProduct.sales_price_min}
</div>
<div className='product-detail-item-label'>Sales Price</div> <div className='product-detail-item-label'>Sales Price</div>
</div> </div>
<div className='product-detail-item'> <div className='product-detail-item'>
@ -53,3 +64,71 @@ export default function ProductDetail() {
</div> </div>
); );
} }
export const CampaignsCollabCreators = () => {
const mockData = [
{
id: 1,
name: 'SUNLINK 拍拍灯',
creatorList: mockCreators,
},
{
id: 2,
name: 'SUNLINK 拍拍灯2',
creatorList: mockCreators,
},
];
if (mockData.length === 0) {
return <div className='text-center'>No campaigns found</div>;
}
return (
<Accordion className='campaigns-collab-creators-list' defaultActiveKey={mockData[0].id}>
这个接口是用哪个根据pid获取关联的活动-根据活动获取creatorList
{mockData.map((item) => (
<Accordion.Item eventKey={item.id} key={item.id} className='campaigns-collab-creators-item'>
<Accordion.Header>{item.name}</Accordion.Header>
<Accordion.Body>
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden'>
<thead>
<tr>
<th>Creator</th>
<th>Category</th>
<th>Followers</th>
<th>GMV Generated</th>
<th>Views Generated</th>
<th>Pricing</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{item?.creatorList.length > 0 &&
item?.creatorList.map((creator) => (
<tr key={creator.id}>
<td>
<div className='white-space-nowrap'>
<img
className='creator-avatar'
src={creator.avatar}
alt={creator.name}
/>
<span className='creator-name'>{creator.name}</span>
</div>
</td>
<td>{creator.category}</td>
<td>{creator.followers || '--'}</td>
<td>{creator.gmv || '--'}</td>
<td>{creator.views || '--'}</td>
<td>{creator.pricing || '--'}</td>
<td>{creator.status || '--'}</td>
</tr>
))}
</tbody>
</Table>
</Accordion.Body>
</Accordion.Item>
))}
</Accordion>
);
};

View File

@ -1,15 +1,20 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Table, Form } from 'react-bootstrap'; import { Table, Form, Button, Modal } from 'react-bootstrap';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
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';
import { Plus } from 'lucide-react';
import SpinningComponent from './Spinning';
import { addProductToCampaign } from '../store/slices/productSlice';
export default function ProductsList({ products, setSelectedProduct, onShowProductDetail }) { export default function ProductsList({ products, onShowProductDetail, type = 'default' }) {
const { selectedBrand } = useSelector((state) => state.brands); const { selectedBrand, status } = useSelector((state) => state.brands);
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 [selectedProducts, setSelectedProducts] = useState([]);
const [showAddProductToCampaignModal, setShowAddProductToCampaignModal] = useState(false);
const [selectedProduct, setSelectedProduct] = useState(null);
const handleSort = (field) => { const handleSort = (field) => {
return; return;
@ -44,10 +49,9 @@ export default function ProductsList({ products, setSelectedProduct, onShowProdu
}; };
if (status === 'loading') { if (status === 'loading') {
return <Spinning />; return <SpinningComponent />;
} }
return ( return (
<div className='products-list rounded shadow-xs'> <div className='products-list rounded shadow-xs'>
<Table responsive hover className='bg-white rounded overflow-hidden m-0'> <Table responsive hover className='bg-white rounded overflow-hidden m-0'>
@ -87,6 +91,7 @@ export default function ProductsList({ products, setSelectedProduct, onShowProdu
<th className='tiktokShop text-center' onClick={() => handleSort('tiktokShop')}> <th className='tiktokShop text-center' onClick={() => handleSort('tiktokShop')}>
TikTok Shop {renderSortIcon('tiktokShop')} TikTok Shop {renderSortIcon('tiktokShop')}
</th> </th>
{type === 'brand' && <th className='actions text-center'>Add To Campaign</th>}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -107,18 +112,24 @@ export default function ProductsList({ products, setSelectedProduct, onShowProdu
/> />
</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' }}
>
<img className='product-logo' src={product.image_url} alt={product.name} /> <img className='product-logo' src={product.image_url} alt={product.name} />
{/* <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>
</td> </td>
<td className='text-center' > <td className='text-center'>
<div>{product.commission_rate}</div> <div>{product.commission_rate}</div>
<div className='small text-muted'>Open collab. {product.open_collab}</div> <div className='small text-muted'>Open collab. {product.open_collab}</div>
</td> </td>
<td className='text-center'>{product.available_samples}</td> <td className='text-center'>{product.available_samples}</td>
<td className='text-center'>{product.sales_price_min} - {product.sales_price_max}</td> <td className='text-center'>
{product.sales_price_min} - {product.sales_price_max}
</td>
<td className='text-center'>{product.stock}</td> <td className='text-center'>{product.stock}</td>
<td className='text-center'>{product.items_sold}</td> <td className='text-center'>{product.items_sold}</td>
<td className='text-center'> <td className='text-center'>
@ -126,12 +137,74 @@ export default function ProductsList({ products, setSelectedProduct, onShowProdu
<div className='small text-muted'>{product.reviews_count} Reviews</div> <div className='small text-muted'>{product.reviews_count} Reviews</div>
</td> </td>
<td className='text-center'>{product.collab_creators}</td> <td className='text-center'>{product.collab_creators}</td>
<td className='text-center'>{product.tiktok_shop && <FontAwesomeIcon icon='fa-brands fa-tiktok' />}</td> <td className='text-center'>
{product.tiktok_shop && <FontAwesomeIcon icon='fa-brands fa-tiktok' />}
</td>
{type === 'brand' && (
<td className='text-center'>
<button className='border-0 bg-transparent text-primary' onClick={() => {
setSelectedProduct(product.id);
setShowAddProductToCampaignModal(true);
}}>
<Plus />
</button>
</td>
)}
</tr> </tr>
)) ))
)} )}
</tbody> </tbody>
</Table> </Table>
<AddProductToCampaignModal show={showAddProductToCampaignModal} onHide={() => setShowAddProductToCampaignModal(false)} productId={selectedProduct} />
</div> </div>
); );
} }
function AddProductToCampaignModal({ show, onHide, productId }) {
const [campaignId, setCampaignId] = useState(null);
const { selectedBrand } = useSelector((state) => state.brands);
const dispatch = useDispatch();
const handleSubmit = async (e) => {
e.preventDefault();
console.log(campaignId, productId);
await dispatch(addProductToCampaign({ campaignId, productId })).unwrap();
handleCancel();
};
const handleCancel = () => {
setCampaignId(null);
onHide();
};
return (
<Modal show={show} onHide={onHide} size='md'>
<Modal.Header closeButton className='fw-bold'>
Add Product
</Modal.Header>
<Form onSubmit={handleSubmit}>
<Modal.Body>
<Form.Group>
<Form.Label>Campaign Name</Form.Label>
<Form.Select required value={campaignId} onChange={(e) => setCampaignId(e.target.value)}>
<option value=''>Select Campaign</option>
{selectedBrand?.campaigns?.map((campaign) => (
<option key={campaign.id} value={campaign.id}>
{campaign.name}
</option>
))}
</Form.Select>
</Form.Group>
</Modal.Body>
<Modal.Footer>
<Button variant='outline-primary' className='border-0' onClick={handleCancel}>
Cancel
</Button>
<Button variant='primary' type='submit'>
Add
</Button>
</Modal.Footer>
</Form>
</Modal>
);
}

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import SearchBar from '../components/SearchBar'; import SearchBar from '../components/SearchBar';
import { Alert, Button, Form, Modal } 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, X } 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';
@ -14,10 +14,11 @@ import {
createCampaignThunk, createCampaignThunk,
} from '../store/slices/brandsSlice'; } from '../store/slices/brandsSlice';
import SlidePanel from '../components/SlidePanel'; import SlidePanel from '../components/SlidePanel';
import ProductDetail from '../components/ProductDetail'; import ProductDetail, { CampaignsCollabCreators } from '../components/ProductDetail';
import { CAMPAIGN_SERVICES, CREATOR_CATEGORIES, CREATOR_LEVELS, CREATOR_TYPES, GMV_RANGES } from '../lib/constant'; import { CAMPAIGN_SERVICES, CREATOR_CATEGORIES, CREATOR_LEVELS, CREATOR_TYPES, GMV_RANGES } from '../lib/constant';
import RangeSlider from '../components/RangeSlider'; import RangeSlider from '../components/RangeSlider';
import SpinningComponent from '../components/Spinning'; import SpinningComponent from '../components/Spinning';
import { setNotificationBarMessage } from '../store/slices/notificationBarSlice';
export default function BrandsDetail() { export default function BrandsDetail() {
const { id } = useParams(); const { id } = useParams();
@ -26,6 +27,7 @@ export default function BrandsDetail() {
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); const [showAddCampaignModal, setShowAddCampaignModal] = useState(false);
const [showAddProductModal, setShowAddProductModal] = useState(false);
useEffect(() => { useEffect(() => {
if (id) { if (id) {
@ -52,7 +54,7 @@ export default function BrandsDetail() {
</Button> </Button>
)} )}
{activeTab === 'products' && ( {activeTab === 'products' && (
<Button> <Button onClick={() => setShowAddProductModal(true)}>
<Plus /> <Plus />
Add Product Add Product
</Button> </Button>
@ -137,6 +139,7 @@ export default function BrandsDetail() {
<ProductsList <ProductsList
products={selectedBrand?.products} products={selectedBrand?.products}
onShowProductDetail={handleShowProductDetail} onShowProductDetail={handleShowProductDetail}
type='brand'
/> />
<SlidePanel <SlidePanel
show={showProductDetail} show={showProductDetail}
@ -144,11 +147,15 @@ export default function BrandsDetail() {
title='Product Detail' title='Product Detail'
size='xxl' size='xxl'
> >
<div className='product-details-panel'>
<ProductDetail /> <ProductDetail />
<CampaignsCollabCreators />
</div>
</SlidePanel> </SlidePanel>
</> </>
)} )}
<AddCampaignModal show={showAddCampaignModal} onHide={() => setShowAddCampaignModal(false)} /> <AddCampaignModal show={showAddCampaignModal} onHide={() => setShowAddCampaignModal(false)} />
<AddProductModal show={showAddProductModal} onHide={() => setShowAddProductModal(false)} />
</React.Fragment> </React.Fragment>
) )
); );
@ -218,7 +225,7 @@ export function AddCampaignModal({ show, onHide }) {
}; };
return ( return (
<Modal show={show} onHide={handleClose} > <Modal show={show} onHide={handleClose}>
<Modal.Header> <Modal.Header>
<Modal.Title>Add Campaign</Modal.Title> <Modal.Title>Add Campaign</Modal.Title>
</Modal.Header> </Modal.Header>
@ -368,3 +375,111 @@ export function AddCampaignModal({ show, onHide }) {
</Modal> </Modal>
); );
} }
export function AddProductModal({ show, onHide }) {
const dispatch = useDispatch();
const [productIds, setProductIds] = useState({
0: '',
});
const addProduct = () => {
const newIndex = Object.keys(productIds).length;
console.log(newIndex);
setProductIds((prev) => ({
...prev,
[newIndex]: '',
}));
};
const handleChange = (e) => {
const { name, value } = e.target;
setProductIds((prev) => ({
...prev,
[name]: value,
}));
};
const handleClose = () => {
setProductIds([]);
onHide();
};
const handleSubmit = (e) => {
e.preventDefault();
dispatch(
setNotificationBarMessage({
message: '这个接口是哪一个?',
variant: 'info',
})
);
const form = document.getElementById('addProductForm');
if (form.checkValidity() === false) {
e.preventDefault();
e.stopPropagation();
setValidated(true);
return;
}
console.log(productIds);
};
const removeProduct = (index) => {
if (Object.keys(productIds).length === 1) {
return;
}
setProductIds((prev) => {
const updated = { ...prev };
delete updated[index];
return updated;
});
};
useEffect(() => {
if (show) {
setProductIds({
0: '',
});
}
}, [show]);
return (
<Modal show={show} onHide={handleClose}>
<Modal.Header>
<Modal.Title>Add Product</Modal.Title>
</Modal.Header>
<Form id='addProductForm' onSubmit={handleSubmit}>
<Modal.Body>
{Object.entries(productIds).map(([index, productId]) => (
<Form.Group key={Number(index)}>
<Form.Label>Product {Number(index) + 1} PID</Form.Label>
<div className='d-flex flex-row align-items-center justify-content-between'>
<Form.Control
type='text'
name={index}
value={productId}
onChange={handleChange}
placeholder='Enter Product ID'
required
/>
<Button variant='link' className='border-0' onClick={() => removeProduct(index)}>
<X />
</Button>
</div>
</Form.Group>
))}
<Button variant='outline-primary' className='mt-3' onClick={addProduct}>
Add Product
</Button>
</Modal.Body>
<Modal.Footer>
<Button variant='outline-primary' className='border-0' onClick={onHide}>
Cancel
</Button>
<Button variant='primary' type='submit'>
Submit
</Button>
</Modal.Footer>
</Form>
</Modal>
);
}

View File

@ -170,7 +170,7 @@ function AddProductModal({ campaignId, show, onHide }) {
}; };
return ( return (
<Modal show={show} onHide={handleCancel}> <Modal show={show} onHide={handleCancel} size='sm'>
<Modal.Header closeButton className='fw-bold'> <Modal.Header closeButton className='fw-bold'>
Add Product Add Product
</Modal.Header> </Modal.Header>

View File

@ -1,10 +1,9 @@
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Accordion, Button, Card, Col, Form, Row, Table } from 'react-bootstrap'; import { Button, Card, Col, Form, Row } 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 ProductDetail, { CampaignsCollabCreators } from '../components/ProductDetail';
import ProductDetail from '../components/ProductDetail';
export default function CampaignScript() { export default function CampaignScript() {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -139,73 +138,6 @@ const CollabInfo = () => {
); );
}; };
const CampaignsCollabCreators = () => {
const mockData = [
{
id: 1,
name: 'SUNLINK 拍拍灯',
creatorList: mockCreators,
},
{
id: 2,
name: 'SUNLINK 拍拍灯2',
creatorList: mockCreators,
},
];
if (mockData.length === 0) {
return <div className='text-center'>No campaigns found</div>;
}
return (
<Accordion className='campaigns-collab-creators-list' defaultActiveKey={mockData[0].id}>
{mockData.map((item) => (
<Accordion.Item eventKey={item.id} key={item.id} className='campaigns-collab-creators-item'>
<Accordion.Header>{item.name}</Accordion.Header>
<Accordion.Body>
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden'>
<thead>
<tr>
<th>Creator</th>
<th>Category</th>
<th>Followers</th>
<th>GMV Generated</th>
<th>Views Generated</th>
<th>Pricing</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{item?.creatorList.length > 0 &&
item?.creatorList.map((creator) => (
<tr key={creator.id}>
<td>
<div className='white-space-nowrap'>
<img
className='creator-avatar'
src={creator.avatar}
alt={creator.name}
/>
<span className='creator-name'>{creator.name}</span>
</div>
</td>
<td>{creator.category}</td>
<td>{creator.followers || '--'}</td>
<td>{creator.gmv || '--'}</td>
<td>{creator.views || '--'}</td>
<td>{creator.pricing || '--'}</td>
<td>{creator.status || '--'}</td>
</tr>
))}
</tbody>
</Table>
</Accordion.Body>
</Accordion.Item>
))}
</Accordion>
);
};
const FileUpload = () => { const FileUpload = () => {
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);

View File

@ -0,0 +1,41 @@
import { createSlice } from '@reduxjs/toolkit';
export const fetchChats = createAsyncThunk('chat/fetchChats', async (_, { rejectWithValue }) => {
try {
const response = await api.get(`/chat-history/search/`);
if (response.code === 200) {
return response.data;
}
throw new Error(response.message);
} catch (error) {
return rejectWithValue(error.message);
}
});
const initialState = {
chats: [],
currentChat: null,
status: 'idle',
error: null,
};
const chatSlice = createSlice({
name: 'chat',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchChats.pending, (state) => {
state.status = 'loading';
})
builder.addCase(fetchChats.fulfilled, (state, action) => {
state.status = 'succeeded';
state.chats = action.payload;
})
builder.addCase(fetchChats.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
});
},
});
export default chatSlice.reducer;

View File

@ -1,4 +1,6 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import api from '@/services/api';
import { setNotificationBarMessage } from './notificationBarSlice';
const mockCreators = [ const mockCreators = [
{ {
@ -22,11 +24,38 @@ const mockCreators = [
date: '2021-01-01', date: '2021-01-01',
}, },
]; ];
export const fetchDiscovery = createAsyncThunk('discovery/fetchDiscovery', async (search) => { export const fetchDiscovery = createAsyncThunk(
// const response = await fetch('/api/discovery'); 'discovery/fetchDiscovery',
// return response.json(); async (searchParams, { rejectWithValue }) => {
return mockCreators; try {
}); const response = await api.post('/creators/search/', searchParams);
if (response.code === 200) {
return response.data;
}
throw new Error(response.message);
} catch (error) {
dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
return rejectWithValue(error.message);
}
}
);
export const fetchDiscoveryByMode = createAsyncThunk(
'discovery/fetchDiscoveryByMode',
async (params, { rejectWithValue }) => {
try {
const response = await api.post('/discovery/creators/search_tags/', params);
if (response.code === 200) {
return response.data;
}
throw new Error(response.message);
} catch (error) {
dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
return rejectWithValue(error.message);
}
}
);
const initialState = { const initialState = {
creators: [], creators: [],
status: 'idle', status: 'idle',
@ -49,6 +78,17 @@ const discoverySlice = createSlice({
.addCase(fetchDiscovery.rejected, (state, action) => { .addCase(fetchDiscovery.rejected, (state, action) => {
state.status = 'failed'; state.status = 'failed';
state.error = action.error.message; state.error = action.error.message;
})
.addCase(fetchDiscoveryByMode.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchDiscoveryByMode.fulfilled, (state, action) => {
state.status = 'succeeded';
state.creators = action.payload;
})
.addCase(fetchDiscoveryByMode.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
}); });
}, },
}); });

View File

@ -1,5 +1,7 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { format, isToday, parseISO } from 'date-fns'; import { format, isToday, parseISO } from 'date-fns';
import api from '@/services/api';
const mockTemplates = [ const mockTemplates = [
{ {
id: 1, id: 1,
@ -169,13 +171,16 @@ const chatDateFormat = (date) => {
} }
}; };
export const fetchInboxList = createAsyncThunk('inbox/fetchInboxList', async () => { export const fetchInboxList = createAsyncThunk('inbox/fetchInboxList', async (_, { rejectWithValue }) => {
await new Promise((resolve) => setTimeout(resolve, 500)); try {
const formattedInboxList = mockInboxList.map((item) => ({ const response = await api.get(`/chat-history/`);
...item, if (response.code === 200) {
date: chatDateFormat(item.date), return response.data;
})); }
return formattedInboxList; throw new Error(response.message);
} catch (error) {
return rejectWithValue(error.message);
}
}); });
export const fetchChatHistory = createAsyncThunk('inbox/fetchChatHistory', async (id) => { export const fetchChatHistory = createAsyncThunk('inbox/fetchChatHistory', async (id) => {

View File

@ -21,8 +21,20 @@ export const addProductToCampaign = createAsyncThunk('products/addProductToCampa
if (response.code !== 201 && response.code !== 200) { if (response.code !== 201 && response.code !== 200) {
throw new Error(response.message); throw new Error(response.message);
} }
console.log(response); dispatch(setNotificationBarMessage({ message: response.message, type: 'success' }));
return response.data;
} catch (error) {
dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
return rejectWithValue(error.message);
}
});
export const fetchProductDetail = createAsyncThunk('products/fetchProductDetail', async (productId, { rejectWithValue, dispatch }) => {
try {
const response = await api.get(`/products/${productId}/`);
if (response.code !== 200) {
throw new Error(response.message);
}
return response.data; return response.data;
} catch (error) { } catch (error) {
dispatch(setNotificationBarMessage({ message: error.message, type: 'error' })); dispatch(setNotificationBarMessage({ message: error.message, type: 'error' }));
@ -34,6 +46,7 @@ const initialState = {
products: [], products: [],
loading: false, loading: false,
error: null, error: null,
productDetail: null,
}; };
const productSlice = createSlice({ const productSlice = createSlice({
@ -63,6 +76,17 @@ const productSlice = createSlice({
state.error = action.payload; state.error = action.payload;
state.loading = false; state.loading = false;
}) })
builder.addCase(fetchProductDetail.pending, (state) => {
state.loading = true;
})
builder.addCase(fetchProductDetail.fulfilled, (state, action) => {
state.productDetail = action.payload;
state.loading = false;
})
builder.addCase(fetchProductDetail.rejected, (state, action) => {
state.error = action.payload;
state.loading = false;
})
}, },
}); });

View File

@ -411,3 +411,18 @@
} }
} }
} }
#addProductForm {
.modal-body {
width: 400px;
display: flex;
flex-flow: column nowrap;
gap: 1rem;
}
}
.product-details-panel {
display: flex;
flex-flow: column nowrap;
gap: 1rem;
}

View File

@ -134,8 +134,3 @@ 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;
}