[dev]creatorList

This commit is contained in:
susie-laptop 2025-05-23 10:27:14 -04:00
parent 443c253014
commit 8dd91b75c8
7 changed files with 196 additions and 64 deletions

View File

@ -25,6 +25,7 @@ export default function CreatorList({ path }) {
hasMore,
isLoadingMore,
pagination,
error,
} = useSelector((state) => state.creators);
const { sortBy, sortDirection } = useSelector((state) => state.filters);
const observer = useRef();
@ -49,7 +50,8 @@ export default function CreatorList({ path }) {
}, [path, dispatch]);
useEffect(() => {
}, [publicCreators]);
console.log(publicCreators, status);
}, [publicCreators, status]);
// /
const handleSelectAll = (e) => {
@ -67,11 +69,13 @@ export default function CreatorList({ path }) {
//
const handleSort = (field) => {
return;
dispatch(setSortBy(field));
};
//
const renderSortIcon = (field) => {
return;
if (sortBy === field) {
return sortDirection === 'asc' ? <ChevronUp size={14} /> : <ChevronDown size={14} />;
}
@ -93,7 +97,7 @@ export default function CreatorList({ path }) {
};
//
if (status === 'loading' && (!publicCreators || publicCreators.length === 0)) {
if (status === 'loading' && !isLoadingMore) {
return (
<div className='text-center p-5'>
<Spinner animation='border' role='status' variant='primary'>
@ -105,7 +109,7 @@ export default function CreatorList({ path }) {
//
if (status === 'failed') {
return <div className='alert alert-danger'>Failed to load creators. Please try again later.</div>;
return <div className='alert alert-danger'>{error || 'Failed to load creators. Please try again later.'}</div>;
}
return (
@ -117,7 +121,9 @@ export default function CreatorList({ path }) {
<th className='selector' style={{ width: '40px' }}>
<Form.Check
type='checkbox'
checked={selectedCreators.length === publicCreators.length && publicCreators.length > 0}
checked={
selectedCreators.length === publicCreators.length && publicCreators.length > 0
}
onChange={handleSelectAll}
/>
</th>

View File

@ -10,8 +10,9 @@ import {
toggleGmvRange,
setViewsRange,
setPricingRange,
setPlatform,
} from '../store/slices/filtersSlice';
import { fetchCreators } from '../store/slices/creatorsSlice';
import { fetchCreators, resetCreators } from '../store/slices/creatorsSlice';
import '../styles/DatabaseFilter.scss';
export default function DatabaseFilter({ path, pageType = 'database' }) {
@ -63,23 +64,23 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
};
//
const [minViews, setMinViews] = useState(filters.viewsRange[0]);
const [maxViews, setMaxViews] = useState(filters.viewsRange[1]);
const [minPricing, setMinPricing] = useState(filters.pricingRange[0]);
const [maxPricing, setMaxPricing] = useState(filters.pricingRange[1]);
const [minViews, setMinViews] = useState(filters.views_range[0]);
const [maxViews, setMaxViews] = useState(filters.views_range[1]);
const [minPricing, setMinPricing] = useState(filters.pricing[0]);
const [maxPricing, setMaxPricing] = useState(filters.pricing[1]);
// Redux
useEffect(() => {
setMinViews(filters.viewsRange[0]);
setMaxViews(filters.viewsRange[1]);
setMinPricing(filters.pricingRange[0]);
setMaxPricing(filters.pricingRange[1]);
}, [filters.viewsRange, filters.pricingRange]);
setMinViews(filters.views_range[0]);
setMaxViews(filters.views_range[1]);
setMinPricing(filters.pricing[0]);
setMaxPricing(filters.pricing[1]);
}, [filters.views_range, filters.pricing]);
//
useEffect(() => {
}, [dispatch, filters, pageType, path]);
dispatch(setPlatform(path));
}, [dispatch, path]);
//
const handleCategorySelect = (category) => {
@ -104,6 +105,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
//
const handleViewsRangeChange = (newRange) => {
dispatch(setViewsRange(newRange));
resetAndFetch();
};
// min input
@ -120,6 +122,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
const handlePricingRangeChange = (newRange) => {
dispatch(setPricingRange(newRange));
resetAndFetch();
};
// min pricing input
@ -145,7 +148,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
const finalValue = Math.min(discreteValue, maxViews);
setMinViews(finalValue);
dispatch(setViewsRange([finalValue, filters.viewsRange[1]]));
dispatch(setViewsRange([finalValue, filters.views_range[1]]));
} else {
//
const closestIndex = findClosestDiscreteIndex(maxViews);
@ -155,7 +158,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
const finalValue = Math.max(discreteValue, minViews);
setMaxViews(finalValue);
dispatch(setViewsRange([filters.viewsRange[0], finalValue]));
dispatch(setViewsRange([filters.views_range[0], finalValue]));
}
};
@ -169,7 +172,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
const finalValue = Math.min(discreteValue, maxPricing);
setMinPricing(finalValue);
dispatch(setPricingRange([finalValue, filters.pricingRange[1]]));
dispatch(setPricingRange([finalValue, filters.pricing[1]]));
} else {
const closestIndex = findClosestDiscreteIndex(maxPricing);
const discreteValue = discretePricingValues[closestIndex];
@ -178,7 +181,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
const finalValue = Math.max(discreteValue, minPricing);
setMaxPricing(finalValue);
dispatch(setPricingRange([filters.pricingRange[0], finalValue]));
dispatch(setPricingRange([filters.pricing[0], finalValue]));
}
};
@ -193,6 +196,43 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
return value;
};
const resetAndFetch = () => {
dispatch(resetCreators());
if (pageType === 'private') {
dispatch(fetchPrivateCreators({ page: 1 }));
} else {
dispatch(fetchCreators({ page: 1 }));
}
};
const onChange = (e) => {
const name = e.target.name;
const value = e.target.value;
switch (name) {
case 'category':
handleCategorySelect(value);
break;
case 'e_commerce_level':
handleEcommerceRatingSelect(value);
break;
case 'exposure_level':
handleExposureRatingSelect(value);
break;
case 'gmv_range':
handleGmvRangeSelect(value);
break;
case 'views_range':
handleViewsRangeChange(value);
break;
case 'pricing':
handlePricingRangeChange(value);
break;
default:
break;
}
resetAndFetch();
};
return (
<div className='shadow-xs mb-4 filter-card'>
<Card.Body>
@ -207,7 +247,9 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
key={category}
variant={filters.category.includes(category) ? 'primary' : 'light'}
className='rounded-pill'
onClick={() => handleCategorySelect(category)}
onClick={onChange}
name='category'
value={category}
>
{category}
</Button>
@ -222,9 +264,11 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
{ecommerceRatings.map((rating) => (
<Button
key={rating}
variant={filters.ecommerceRatings.includes(rating) ? 'primary' : 'light'}
variant={filters.e_commerce_level.includes(rating) ? 'primary' : 'light'}
className='rounded-pill'
onClick={() => handleEcommerceRatingSelect(rating)}
onClick={onChange}
name='e_commerce_level'
value={rating}
>
{rating}
</Button>
@ -239,9 +283,11 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
{exposureRatings.map((rating) => (
<Button
key={rating}
variant={filters.exposureRatings.includes(rating) ? 'primary' : 'light'}
variant={filters.exposure_level.includes(rating) ? 'primary' : 'light'}
className='rounded-pill'
onClick={() => handleExposureRatingSelect(rating)}
onClick={onChange}
name='exposure_level'
value={rating}
>
{rating}
</Button>
@ -256,9 +302,11 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
{gmvRanges.map((range) => (
<Button
key={range.label}
variant={filters.gmvRanges.includes(range.label) ? 'primary' : 'light'}
variant={filters.gmv_range.includes(range.label) ? 'primary' : 'light'}
className='rounded-pill'
onClick={() => handleGmvRangeSelect(range.label)}
onClick={onChange}
name='gmv_range'
value={range.label}
>
{range.label}
</Button>
@ -273,7 +321,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
<RangeSlider
min={0}
max={500000}
value={filters.viewsRange}
value={filters.views_range}
onChange={handleViewsRangeChange}
/>
@ -313,7 +361,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
<RangeSlider
min={0}
max={500000}
value={filters.pricingRange}
value={filters.pricing}
onChange={handlePricingRangeChange}
/>

View File

@ -0,0 +1,21 @@
import { Check, CircleAlert, Info, X } from 'lucide-react';
import { useDispatch } from 'react-redux';
export default function NotificationBar() {
const { message, type, show } = useSelector((state) => state.notificationBar);
const dispatch = useDispatch();
return (
<div
className={`snackbar alert alert-${type} d-flex align-items-center justify-content-between position-fixed top-10 start-50 translate-middle w-50 gap-2`}
role='alert'
>
{type === 'success' && <Check />}
{type === 'warning' && <CircleAlert />}
{type === 'error' && <X />}
{type === 'info' && <Info />}
<div className='flex-fill'>{message}</div>
<button type='button' className='btn-close flex-end' onClick={handleClose} aria-label='Close'></button>
</div>
);
}

View File

@ -15,7 +15,7 @@ import { Link } from 'react-router-dom';
export default function PrivateCreatorList({ path }) {
const dispatch = useDispatch();
const { privateCreators, status, selectedCreators, hasMore, isLoadingMore, pagination } = useSelector(
const { privateCreators, status, selectedCreators, hasMore, isLoadingMore, pagination, error } = useSelector(
(state) => state.creators
);
const { sortBy, sortDirection } = useSelector((state) => state.filters);
@ -60,11 +60,13 @@ export default function PrivateCreatorList({ path }) {
//
const handleSort = (field) => {
return;
dispatch(setSortBy(field));
};
//
const renderSortIcon = (field) => {
return;
if (sortBy === field) {
return sortDirection === 'asc' ? <ChevronUp size={14} /> : <ChevronDown size={14} />;
}
@ -86,7 +88,7 @@ export default function PrivateCreatorList({ path }) {
};
//
if (status === 'loading' && (!privateCreators || privateCreators.length === 0)) {
if (status === 'loading' && !isLoadingMore) {
return (
<div className='text-center p-5'>
<Spinner animation='border' role='status' variant='primary'>
@ -98,7 +100,7 @@ export default function PrivateCreatorList({ path }) {
//
if (status === 'failed') {
return <div className='alert alert-danger'>Failed to load creators. Please try again later.</div>;
return <div className='alert alert-danger'>{error || 'Failed to load creators. Please try again later.'}</div>;
}
return (

View File

@ -216,24 +216,47 @@ const initialState = {
isLoadingMore: false,
};
export const fetchCreators = createAsyncThunk('creators/fetchCreators', async ({ path, page = 1 }, { getState }) => {
export const fetchCreators = createAsyncThunk(
'creators/fetchCreators',
async ({ page = 1 }, { getState, rejectWithValue }) => {
try {
const state = getState();
const filters = state.filters;
const response = await api.get(`/daren_detail/public/creators`, { params: { page } });
console.log(response);
return response;
});
const { code, data, message, pagination } = await api.post(
`/daren_detail/public/creators/filter/?page=${page}`,
filters
);
if (code === 200) {
return { data, pagination };
} else {
throw new Error(message);
}
} catch (error) {
return rejectWithValue(error.message);
}
}
);
export const fetchPrivateCreators = createAsyncThunk(
'creators/fetchPrivateCreators',
async ({ path, page = 1 }, { getState }) => {
async ({ page = 1 }, { getState, rejectWithValue, dispatch }) => {
try {
const state = getState();
const filters = state.filters;
const queryParams = { pool_id: 1, page };
const response = await api.get(`/daren_detail/private/pools/creators`, { params: queryParams });
return response;
const { code, data, message, pagination } = await api.post(
`/daren_detail/private/pools/creators/filter/?page=${page}`,
{ pool_id: 1, filters }
);
if (code === 200) {
return { data, pagination };
} else {
throw new Error(message);
}
} catch (error) {
return rejectWithValue(error.message);
}
}
);
@ -283,8 +306,8 @@ const creatorsSlice = createSlice({
extraReducers: (builder) => {
builder
.addCase(fetchCreators.pending, (state) => {
if (state.publicCreators.length === 0) {
state.status = 'loading';
if (state.publicCreators.length === 0) {
} else {
state.isLoadingMore = true;
}
@ -304,7 +327,7 @@ const creatorsSlice = createSlice({
.addCase(fetchCreators.rejected, (state, action) => {
state.status = 'failed';
state.isLoadingMore = false;
state.error = action.error.message;
state.error = action.payload;
})
.addCase(fetchPrivateCreators.pending, (state) => {
if (state.privateCreators?.length === 0) {
@ -330,7 +353,7 @@ const creatorsSlice = createSlice({
console.log('fetchPrivateCreators.rejected', action);
state.status = 'failed';
state.isLoadingMore = false;
state.error = action.error.message;
state.error = action.payload;
})
.addCase(fetchCreatorDetail.pending, (state) => {
state.status = 'loading';

View File

@ -2,13 +2,14 @@ import { createSlice } from '@reduxjs/toolkit';
const initialState = {
category: ['Homes Supplies'],
ecommerceRatings: ['L2', 'L3'],
exposureRatings: [],
gmvRanges: ['$5k - $25k', '$25k - $60k'],
viewsRange: [0, 100000],
pricingRange: [0, 3000],
e_commerce_level: ['L2', 'L3'],
exposure_level: [],
gmv_range: ['$5k - $25k', '$25k - $60k'],
views_range: [0, 100000],
pricing: [0, 3000],
sortBy: 'followers',
sortDirection: 'desc',
platform: '',
};
const filtersSlice = createSlice({
name: 'filters',
@ -24,33 +25,33 @@ const filtersSlice = createSlice({
},
toggleEcommerceRating: (state, action) => {
const rating = action.payload;
if (state.ecommerceRatings.includes(rating)) {
state.ecommerceRatings = state.ecommerceRatings.filter((r) => r !== rating);
if (state.e_commerce_level.includes(rating)) {
state.e_commerce_level = state.e_commerce_level.filter((r) => r !== rating);
} else {
state.ecommerceRatings.push(rating);
state.e_commerce_level.push(rating);
}
},
toggleExposureRating: (state, action) => {
const rating = action.payload;
if (state.exposureRatings.includes(rating)) {
state.exposureRatings = state.exposureRatings.filter((r) => r !== rating);
if (state.exposure_level.includes(rating)) {
state.exposure_level = state.exposure_level.filter((r) => r !== rating);
} else {
state.exposureRatings.push(rating);
state.exposure_level.push(rating);
}
},
toggleGmvRange: (state, action) => {
const range = action.payload;
if (state.gmvRanges.includes(range)) {
state.gmvRanges = state.gmvRanges.filter((r) => r !== range);
if (state.gmv_range.includes(range)) {
state.gmv_range = state.gmv_range.filter((r) => r !== range);
} else {
state.gmvRanges.push(range);
state.gmv_range.push(range);
}
},
setViewsRange: (state, action) => {
state.viewsRange = action.payload;
state.views_range = action.payload;
},
setPricingRange: (state, action) => {
state.pricingRange = action.payload;
state.pricing = action.payload;
},
setSortBy: (state, action) => {
// 如果选择了当前已激活的排序项,则切换排序方向
@ -62,6 +63,9 @@ const filtersSlice = createSlice({
state.sortDirection = 'desc';
}
},
setPlatform: (state, action) => {
state.platform = action.payload;
},
resetFilters: () => {
return initialState;
},
@ -76,6 +80,7 @@ export const {
setViewsRange,
setPricingRange,
setSortBy,
setPlatform,
resetFilters,
} = filtersSlice.actions;

View File

@ -0,0 +1,27 @@
const initialState = {
message: '',
show: false,
type: 'success', // success, warning, error, info
};
const notificationBarSlice = createSlice({
name: 'notificationBar',
initialState,
reducers: {
setNotificationBarMessage: (state, action) => {
state.message = action.payload;
},
setNotificationBarShow: (state, action) => {
state.show = action.payload;
},
resetNotificationBar: (state) => {
state.message = '';
state.show = false;
state.type = 'success';
},
},
});
export const { setNotificationBarMessage, setNotificationBarShow, resetNotificationBar } = notificationBarSlice.actions;
export default notificationBarSlice.reducer;