Compare commits

...

3 Commits

Author SHA1 Message Date
443c253014 [dev]fix creators slice 2025-05-22 21:58:48 -04:00
4c22e12101 [dev]brands list 2025-05-22 21:42:59 -04:00
c518639845 [dev]login 2025-05-22 21:22:51 -04:00
15 changed files with 226 additions and 78 deletions

View File

@ -33,14 +33,14 @@ export default function BrandsList({ openBrandDetail }) {
<Hash size={16} /> <Hash size={16} />
Collab. Collab.
</span> </span>
<span className='card-text-content'>{brand.collab}</span> <span className='card-text-content'>{brand.collab_count}</span>
</Card.Text> </Card.Text>
<Card.Text className=''> <Card.Text className=''>
<span className='card-text-title'> <span className='card-text-title'>
<Users size={16} /> <Users size={16} />
Creators Creators
</span> </span>
<span className='card-text-content'>{brand.creators}</span> <span className='card-text-content'>{brand.creators_count}</span>
</Card.Text> </Card.Text>
<Card.Text className=''> <Card.Text className=''>
<span className='card-text-title'> <span className='card-text-title'>

View File

@ -3,14 +3,23 @@ import { useEffect } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ChartNoAxesColumnIncreasing, CircleDollarSign, Edit, Eye, Folders, Hash, Layers, Tag, TrendingUp, UserRoundCheck } from 'lucide-react'; import { ChartNoAxesColumnIncreasing, CircleDollarSign, Edit, Eye, Folders, Hash, Layers, Tag, TrendingUp, UserRoundCheck } from 'lucide-react';
import Spinning from './Spinning';
export default function CampaignList() { export default function CampaignList() {
const { selectedBrand } = useSelector((state) => state.brands); const { selectedBrand ,status } = useSelector((state) => state.brands);
useEffect(() => { useEffect(() => {
console.log(selectedBrand); console.log(selectedBrand);
}, [selectedBrand]); }, [selectedBrand]);
if (status === 'loading') {
return <Spinning />;
}
if (selectedBrand?.campaigns?.length === 0) {
return <div>No campaigns found</div>;
}
return ( return (
<div className='campaigns-list'> <div className='campaigns-list'>
{selectedBrand?.campaigns?.length > 0 && {selectedBrand?.campaigns?.length > 0 &&

View File

@ -54,7 +54,7 @@ export default function CreatorList({ path }) {
// / // /
const handleSelectAll = (e) => { const handleSelectAll = (e) => {
if (e.target.checked) { if (e.target.checked) {
dispatch(selectAllCreators()); dispatch(selectAllCreators('database'));
} else { } else {
dispatch(clearCreatorSelection()); dispatch(clearCreatorSelection());
} }

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { Nav, Accordion } from 'react-bootstrap'; import { Nav, Accordion, Button } from 'react-bootstrap';
import { import {
Settings, Settings,
ChevronDown, ChevronDown,
@ -12,10 +12,12 @@ import {
Heart, Heart,
Send, Send,
FileText, FileText,
LogOut,
} from 'lucide-react'; } from 'lucide-react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import logo from '@/assets/logo.png'; import logo from '@/assets/logo.png';
import '@/styles/sidebar.scss'; import '@/styles/sidebar.scss';
import { useDispatch } from 'react-redux';
// Organized menu items // Organized menu items
const menuItems = [ const menuItems = [
@ -233,6 +235,11 @@ export default function Sidebar() {
} }
})} })}
</Nav> </Nav>
<div className='sidebar-footer'>
<Button variant='outline-danger'>
<LogOut />
</Button>
</div>
</div> </div>
); );
} }

View File

@ -47,7 +47,7 @@ export default function PrivateCreatorList({ path }) {
// / // /
const handleSelectAll = (e) => { const handleSelectAll = (e) => {
if (e.target.checked) { if (e.target.checked) {
dispatch(selectAllCreators()); dispatch(selectAllCreators('database'));
} else { } else {
dispatch(clearCreatorSelection()); dispatch(clearCreatorSelection());
} }

View File

@ -4,23 +4,16 @@ import { useSelector, useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import '../styles/Products.scss'; import '../styles/Products.scss';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Spinning from './Spinning';
export default function ProductsList({ onShowProductDetail }) { export default function ProductsList({ onShowProductDetail }) {
const { brandId } = useParams(); const { brandId } = useParams();
const { brands } = useSelector((state) => state.brands); const { selectedBrand } = useSelector((state) => state.brands);
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
const [selectedProducts, setSelectedProducts] = useState([]); const [selectedProducts, setSelectedProducts] = useState([]);
const [sortField, setSortField] = useState(null); const [sortField, setSortField] = useState(null);
const [sortDirection, setSortDirection] = useState('asc'); const [sortDirection, setSortDirection] = useState('asc');
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) => { const handleSort = (field) => {
if (sortField === field) { if (sortField === field) {
@ -52,6 +45,11 @@ export default function ProductsList({ onShowProductDetail }) {
} }
}; };
if (status === 'loading') {
return <Spinning />;
}
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'>
@ -60,7 +58,7 @@ export default function ProductsList({ onShowProductDetail }) {
<th className='selector' style={{ width: '40px' }}> <th className='selector' style={{ width: '40px' }}>
<Form.Check <Form.Check
type='checkbox' type='checkbox'
checked={selectedProducts.length === products.length && products.length > 0} checked={selectedProducts.length === selectedBrand?.products?.length && selectedBrand?.products?.length > 0}
onChange={handleSelectAll} onChange={handleSelectAll}
/> />
</th> </th>
@ -94,14 +92,14 @@ export default function ProductsList({ onShowProductDetail }) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{products.length === 0 ? ( {selectedBrand?.products?.length === 0 ? (
<tr> <tr>
<td colSpan='10' className='text-center py-4'> <td colSpan='10' className='text-center py-4'>
No products found for this brand. No products found for this brand.
</td> </td>
</tr> </tr>
) : ( ) : (
products.map((product) => ( selectedBrand?.products?.map((product) => (
<tr key={product.id} className={selectedProducts.includes(product.id) ? 'selected' : ''}> <tr key={product.id} className={selectedProducts.includes(product.id) ? 'selected' : ''}>
<td> <td>
<Form.Check <Form.Check
@ -117,19 +115,19 @@ export default function ProductsList({ onShowProductDetail }) {
</div> </div>
</td> </td>
<td className='text-center' > <td className='text-center' >
<div>{product.commission}</div> <div>{product.commission_rate}</div>
<div className='small text-muted'>Open collab. {product.openCollab}</div> <div className='small text-muted'>Open collab. {product.open_collab}</div>
</td> </td>
<td className='text-center'>{product.availableSamples}</td> <td className='text-center'>{product.available_samples}</td>
<td className='text-center'>{product.price}</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.sold}</td> <td className='text-center'>{product.items_sold}</td>
<td className='text-center'> <td className='text-center'>
<div>{product.rating}</div> <div>{product.product_rating}</div>
<div className='small text-muted'>{product.reviews} Reviews</div> <div className='small text-muted'>{product.reviews_count} Reviews</div>
</td> </td>
<td className='text-center'>{product.collabCreators}</td> <td className='text-center'>{product.collab_creators}</td>
<td className='text-center'>{product.tiktokShop && <FontAwesomeIcon icon='fa-brands fa-tiktok' />}</td> <td className='text-center'>{product.tiktok_shop && <FontAwesomeIcon icon='fa-brands fa-tiktok' />}</td>
</tr> </tr>
)) ))
)} )}

View File

@ -6,7 +6,7 @@ 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 { findBrandById } from '../store/slices/brandsSlice'; import { findBrandById, fetchBrandCampaigns, fetchBrandProducts } from '../store/slices/brandsSlice';
export default function BrandsDetail() { export default function BrandsDetail() {
const { id } = useParams(); const { id } = useParams();
@ -16,9 +16,9 @@ export default function BrandsDetail() {
useEffect(() => { useEffect(() => {
if (id) { if (id) {
console.log(id);
dispatch(findBrandById(id)); dispatch(findBrandById(id));
dispatch(fetchBrandCampaigns(id));
dispatch(fetchBrandProducts(id));
} }
}, [dispatch, id]); }, [dispatch, id]);
@ -69,31 +69,31 @@ export default function BrandsDetail() {
<Hash size={20} /> <Hash size={20} />
Collab. Collab.
</div> </div>
<div className='info-value'>{selectedBrand.collab}</div> <div className='info-value'>{selectedBrand.collab_count}</div>
</div> </div>
<div className='info-item'> <div className='info-item'>
<div className='info-name'> <div className='info-name'>
<Users size={20} /> <Users size={20} />
Creators Creators
</div> </div>
<div className='info-value'>{selectedBrand.creators}</div> <div className='info-value'>{selectedBrand.creators_count}</div>
</div> </div>
</div> </div>
<div className='brand-info-bottom'> <div className='brand-info-bottom'>
<div className='info-item'> <div className='info-item'>
<div className='info-value'>{selectedBrand.collab}</div> <div className='info-value'>{selectedBrand.creators_count}</div>
<div className='info-name'>Total Collab. Creators</div> <div className='info-name'>Total Collab. Creators</div>
</div> </div>
<div className='info-item'> <div className='info-item'>
<div className='info-value'>{selectedBrand.collab}</div> <div className='info-value'>{selectedBrand.total_gmv_achieved}</div>
<div className='info-name'>Total GMV Achieved</div> <div className='info-name'>Total GMV Achieved</div>
</div> </div>
<div className='info-item'> <div className='info-item'>
<div className='info-value'>{selectedBrand.collab}</div> <div className='info-value'>{selectedBrand.total_views_achieved}</div>
<div className='info-name'>Total Views Achieved</div> <div className='info-name'>Total Views Achieved</div>
</div> </div>
<div className='info-item'> <div className='info-item'>
<div className='info-value'>{selectedBrand.collab}</div> <div className='info-value'>{selectedBrand.shop_overall_rating}</div>
<div className='info-name'>Shop Overall Rating</div> <div className='info-name'>Shop Overall Rating</div>
</div> </div>
</div> </div>

View File

@ -1,17 +1,50 @@
import '@/styles/Login.scss'; import '@/styles/Login.scss';
import { Button, Form, InputGroup } from 'react-bootstrap'; import { Button, Form, InputGroup } from 'react-bootstrap';
import { LockKeyhole, User } from 'lucide-react'; import { LockKeyhole, User } from 'lucide-react';
import { useDispatch, useSelector } from 'react-redux';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { loginThunk } from '../store/slices/authSlice';
export default function Login() { export default function Login() {
const [formData, setFormData] = useState({
email: '',
password: '',
});
const { isLoading, isAuthenticated } = useSelector((state) => state.auth);
const navigate = useNavigate();
const dispatch = useDispatch();
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = (e) => { const handleSubmit = (e) => {
if (!handleValidate()) {
return;
}
e.preventDefault(); e.preventDefault();
console.log('Form submitted'); console.log('Form submitted');
dispatch(loginThunk(formData));
}; };
useEffect(() => {
if (isAuthenticated) {
navigate('/');
}
}, [isAuthenticated, navigate]);
const handleValidate = () => {
if (formData.email === '' || formData.password === '') {
return false;
}
return true;
};
return ( return (
<div className='login-container'> <div className='login-container'>
<div className='title'>Creator Center</div> <div className='title'>Creator Center</div>
<Form className='login-form' onSubmit={handleSubmit}> <Form className='login-form' onSubmit={handleSubmit}>
<Form.Group> {/* <Form.Group>
<Form.Label>Username</Form.Label> <Form.Label>Username</Form.Label>
<InputGroup> <InputGroup>
<InputGroup.Text> <InputGroup.Text>
@ -19,6 +52,22 @@ export default function Login() {
</InputGroup.Text> </InputGroup.Text>
<Form.Control type='text' placeholder='Enter username' /> <Form.Control type='text' placeholder='Enter username' />
</InputGroup> </InputGroup>
</Form.Group> */}
<Form.Group>
<Form.Label>Email</Form.Label>
<InputGroup>
<InputGroup.Text>
<User />
</InputGroup.Text>
<Form.Control
required
type='email'
placeholder='Enter email'
name='email'
value={formData.email}
onChange={handleChange}
/>
</InputGroup>
</Form.Group> </Form.Group>
<Form.Group> <Form.Group>
<Form.Label>Password</Form.Label> <Form.Label>Password</Form.Label>
@ -26,10 +75,19 @@ export default function Login() {
<InputGroup.Text> <InputGroup.Text>
<LockKeyhole /> <LockKeyhole />
</InputGroup.Text> </InputGroup.Text>
<Form.Control type='password' placeholder='Enter password' /> <Form.Control
required
type='password'
placeholder='Enter password'
name='password'
value={formData.password}
onChange={handleChange}
/>
</InputGroup> </InputGroup>
</Form.Group> </Form.Group>
<Button type='submit'>Sign In</Button> <Button type='submit' loading={isLoading} disabled={!handleValidate()}>
Sign In
</Button>
</Form> </Form>
</div> </div>
); );

View File

@ -1,22 +1,23 @@
import axios from 'axios'; import axios from 'axios';
import store, { dispatch } from '@/store';
import { clearUser } from '../store/slices/authSlice';
const api = axios.create({ const api = axios.create({
baseURL: '/api', baseURL: '/api',
withCredentials: true, // Include cookies if needed withCredentials: true, // Include cookies if needed
}); });
api.interceptors.request.use( api.interceptors.request.use(
(config) => { (config) => {
const token = sessionStorage.getItem('token') || '03d9163150a8bfbbee2a33c4444237f337a35278'; const token = sessionStorage.getItem('token') || '';
if (token) { if (token) {
config.headers.Authorization = `Token ${token}`; config.headers.Authorization = `Token ${token}`;
} }
return config; return config;
}, },
(error) => { (error) => {
console.error('Request error:', error); console.error('Request error:', error);
return Promise.reject(error); return Promise.reject(error);
} }
); );
api.interceptors.response.use( api.interceptors.response.use(
@ -24,6 +25,11 @@ api.interceptors.response.use(
return response; return response;
}, },
(error) => { (error) => {
if (error.response.status === 401) {
sessionStorage.removeItem('token');
window.location.href = '/login';
dispatch(clearUser());
}
return Promise.reject(error); return Promise.reject(error);
} }
); );
@ -49,13 +55,12 @@ const del = async (url) => {
}; };
const upload = async (url, data) => { const upload = async (url, data) => {
const response = await api.post(url, data, { const response = await api.post(url, data, {
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
}); });
return response.data; return response.data;
}; };
export default { get, post, put, del, upload }; export default { get, post, put, del, upload };

View File

@ -33,5 +33,5 @@ const store = configureStore({
}); });
export const persistor = persistStore(store); export const persistor = persistStore(store);
export default store; export default store;
export const dispatch = store.dispatch;

View File

@ -1,9 +1,24 @@
import { createSlice } from "@reduxjs/toolkit"; import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import api from '@/services/api';
export const loginThunk = createAsyncThunk('auth/login', async (credentials, { rejectWithValue }) => {
try {
const response = await api.post('/user/login/', credentials);
if (response.code === 200) {
sessionStorage.setItem('token', response.data.token);
return response.data;
} else {
return rejectWithValue(response.message);
}
} catch (error) {
return rejectWithValue(error.message);
}
});
const initialState = { const initialState = {
user: null, user: null,
token: null,
isAuthenticated: false, isAuthenticated: false,
isLoading: false,
}; };
const authSlice = createSlice({ const authSlice = createSlice({
@ -13,8 +28,26 @@ const authSlice = createSlice({
setUser: (state, action) => { setUser: (state, action) => {
state.user = action.payload; state.user = action.payload;
}, },
clearUser: (state) => {
state.user = null;
state.isAuthenticated = false;
},
},
extraReducers: (builder) => {
builder.addCase(loginThunk.pending, (state, action) => {
state.isLoading = true;
});
builder.addCase(loginThunk.fulfilled, (state, action) => {
state.user = action.payload;
state.isAuthenticated = true;
state.isLoading = false;
});
builder.addCase(loginThunk.rejected, (state, action) => {
state.isAuthenticated = false;
state.isLoading = false;
});
}, },
}); });
export const { setUser } = authSlice.actions; export const { setUser, clearUser } = authSlice.actions;
export default authSlice.reducer; export default authSlice.reducer;

View File

@ -1,5 +1,5 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import api from '@/services/api';
const mockProducts = [ const mockProducts = [
{ {
id: 1, id: 1,
@ -108,10 +108,19 @@ const mockBrands = [
export const fetchBrands = createAsyncThunk('brands/fetchBrands', async () => { export const fetchBrands = createAsyncThunk('brands/fetchBrands', async () => {
// const response = await fetch('https://api.example.com/brands'); // const response = await fetch('https://api.example.com/brands');
await new Promise((resolve) => setTimeout(resolve, 500)); const response = await api.get('/brands/');
console.log('fetchBrands'); return response.data;
});
return mockBrands; export const fetchBrandCampaigns = createAsyncThunk('brands/fetchBrandCampaigns', async (brandId) => {
const response = await api.get(`/brands/${brandId}/campaigns/`);
return response.data;
});
export const fetchBrandProducts = createAsyncThunk('brands/fetchBrandProducts', async (brandId) => {
const response = await api.get(`/brands/${brandId}/products/`);
console.log(response);
return response.data;
}); });
const initialState = { const initialState = {
@ -161,6 +170,28 @@ const brandsSlice = createSlice({
.addCase(fetchBrands.rejected, (state, action) => { .addCase(fetchBrands.rejected, (state, action) => {
state.status = 'failed'; state.status = 'failed';
state.error = action.error.message; state.error = action.error.message;
})
.addCase(fetchBrandCampaigns.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchBrandCampaigns.fulfilled, (state, action) => {
state.status = 'succeeded';
state.selectedBrand.campaigns = action.payload;
})
.addCase(fetchBrandCampaigns.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
})
.addCase(fetchBrandProducts.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchBrandProducts.fulfilled, (state, action) => {
state.status = 'succeeded';
state.selectedBrand.products = action.payload;
})
.addCase(fetchBrandProducts.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
}); });
}, },
}); });

View File

@ -1,5 +1,6 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import api from '../../services/api'; import api from '@/services/api';
const mockVideos = [ const mockVideos = [
{ {
id: 1, id: 1,
@ -220,6 +221,7 @@ export const fetchCreators = createAsyncThunk('creators/fetchCreators', async ({
const filters = state.filters; const filters = state.filters;
const response = await api.get(`/daren_detail/public/creators`, { params: { page } }); const response = await api.get(`/daren_detail/public/creators`, { params: { page } });
console.log(response);
return response; return response;
}); });
@ -257,8 +259,12 @@ const creatorsSlice = createSlice({
state.selectedCreators.push(creatorId); state.selectedCreators.push(creatorId);
} }
}, },
selectAllCreators: (state) => { selectAllCreators: (state, action) => {
state.selectedCreators = state.creators.map((creator) => creator.id); if (action.payload === 'database') {
state.selectedCreators = state.publicCreators.map((creator) => creator.id);
} else {
state.selectedCreators = state.privateCreators.map((creator) => creator.id);
}
}, },
clearCreatorSelection: (state) => { clearCreatorSelection: (state) => {
state.selectedCreators = []; state.selectedCreators = [];
@ -277,7 +283,7 @@ const creatorsSlice = createSlice({
extraReducers: (builder) => { extraReducers: (builder) => {
builder builder
.addCase(fetchCreators.pending, (state) => { .addCase(fetchCreators.pending, (state) => {
if (state.creators.length === 0) { if (state.publicCreators.length === 0) {
state.status = 'loading'; state.status = 'loading';
} else { } else {
state.isLoadingMore = true; state.isLoadingMore = true;
@ -287,7 +293,6 @@ const creatorsSlice = createSlice({
state.status = 'succeeded'; state.status = 'succeeded';
state.isLoadingMore = false; state.isLoadingMore = false;
const { data, pagination } = action.payload; const { data, pagination } = action.payload;
if (pagination.current_page === 1) { if (pagination.current_page === 1) {
state.publicCreators = data; state.publicCreators = data;
} else { } else {
@ -302,7 +307,7 @@ const creatorsSlice = createSlice({
state.error = action.error.message; state.error = action.error.message;
}) })
.addCase(fetchPrivateCreators.pending, (state) => { .addCase(fetchPrivateCreators.pending, (state) => {
if (state.creators?.length === 0) { if (state.privateCreators?.length === 0) {
state.status = 'loading'; state.status = 'loading';
} else { } else {
state.isLoadingMore = true; state.isLoadingMore = true;
@ -332,7 +337,7 @@ const creatorsSlice = createSlice({
}) })
.addCase(fetchCreatorDetail.fulfilled, (state, action) => { .addCase(fetchCreatorDetail.fulfilled, (state, action) => {
state.status = 'succeeded'; state.status = 'succeeded';
const { data, pagination } = action.payload; const { data } = action.payload;
state.selectedCreator = data; state.selectedCreator = data;
}) })
.addCase(fetchCreatorDetail.rejected, (state, action) => { .addCase(fetchCreatorDetail.rejected, (state, action) => {

View File

@ -8,7 +8,8 @@
gap: 1rem; gap: 1rem;
.brand-card { .brand-card {
width: 325px; flex: 1;
min-width: 325px;
padding: 1.5rem; padding: 1.5rem;
border-radius: 0.4rem; border-radius: 0.4rem;
background-color: white; background-color: white;

View File

@ -31,6 +31,7 @@
.btn { .btn {
display: inline-flex !important; display: inline-flex !important;
align-items: center; align-items: center;
justify-content: center;
} }
.back-button { .back-button {
width: max-content; width: max-content;