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

View File

@ -10,8 +10,9 @@ import {
toggleGmvRange, toggleGmvRange,
setViewsRange, setViewsRange,
setPricingRange, setPricingRange,
setPlatform,
} from '../store/slices/filtersSlice'; } from '../store/slices/filtersSlice';
import { fetchCreators } from '../store/slices/creatorsSlice'; import { fetchCreators, resetCreators } from '../store/slices/creatorsSlice';
import '../styles/DatabaseFilter.scss'; import '../styles/DatabaseFilter.scss';
export default function DatabaseFilter({ path, pageType = 'database' }) { 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 [minViews, setMinViews] = useState(filters.views_range[0]);
const [maxViews, setMaxViews] = useState(filters.viewsRange[1]); const [maxViews, setMaxViews] = useState(filters.views_range[1]);
const [minPricing, setMinPricing] = useState(filters.pricingRange[0]); const [minPricing, setMinPricing] = useState(filters.pricing[0]);
const [maxPricing, setMaxPricing] = useState(filters.pricingRange[1]); const [maxPricing, setMaxPricing] = useState(filters.pricing[1]);
// Redux // Redux
useEffect(() => { useEffect(() => {
setMinViews(filters.viewsRange[0]); setMinViews(filters.views_range[0]);
setMaxViews(filters.viewsRange[1]); setMaxViews(filters.views_range[1]);
setMinPricing(filters.pricingRange[0]); setMinPricing(filters.pricing[0]);
setMaxPricing(filters.pricingRange[1]); setMaxPricing(filters.pricing[1]);
}, [filters.viewsRange, filters.pricingRange]); }, [filters.views_range, filters.pricing]);
// //
useEffect(() => { useEffect(() => {
dispatch(setPlatform(path));
}, [dispatch, filters, pageType, path]); }, [dispatch, path]);
// //
const handleCategorySelect = (category) => { const handleCategorySelect = (category) => {
@ -104,6 +105,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
// //
const handleViewsRangeChange = (newRange) => { const handleViewsRangeChange = (newRange) => {
dispatch(setViewsRange(newRange)); dispatch(setViewsRange(newRange));
resetAndFetch();
}; };
// min input // min input
@ -120,6 +122,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
const handlePricingRangeChange = (newRange) => { const handlePricingRangeChange = (newRange) => {
dispatch(setPricingRange(newRange)); dispatch(setPricingRange(newRange));
resetAndFetch();
}; };
// min pricing input // min pricing input
@ -145,7 +148,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
const finalValue = Math.min(discreteValue, maxViews); const finalValue = Math.min(discreteValue, maxViews);
setMinViews(finalValue); setMinViews(finalValue);
dispatch(setViewsRange([finalValue, filters.viewsRange[1]])); dispatch(setViewsRange([finalValue, filters.views_range[1]]));
} else { } else {
// //
const closestIndex = findClosestDiscreteIndex(maxViews); const closestIndex = findClosestDiscreteIndex(maxViews);
@ -155,7 +158,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
const finalValue = Math.max(discreteValue, minViews); const finalValue = Math.max(discreteValue, minViews);
setMaxViews(finalValue); 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); const finalValue = Math.min(discreteValue, maxPricing);
setMinPricing(finalValue); setMinPricing(finalValue);
dispatch(setPricingRange([finalValue, filters.pricingRange[1]])); dispatch(setPricingRange([finalValue, filters.pricing[1]]));
} else { } else {
const closestIndex = findClosestDiscreteIndex(maxPricing); const closestIndex = findClosestDiscreteIndex(maxPricing);
const discreteValue = discretePricingValues[closestIndex]; const discreteValue = discretePricingValues[closestIndex];
@ -178,7 +181,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
const finalValue = Math.max(discreteValue, minPricing); const finalValue = Math.max(discreteValue, minPricing);
setMaxPricing(finalValue); 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; 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 ( return (
<div className='shadow-xs mb-4 filter-card'> <div className='shadow-xs mb-4 filter-card'>
<Card.Body> <Card.Body>
@ -207,7 +247,9 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
key={category} key={category}
variant={filters.category.includes(category) ? 'primary' : 'light'} variant={filters.category.includes(category) ? 'primary' : 'light'}
className='rounded-pill' className='rounded-pill'
onClick={() => handleCategorySelect(category)} onClick={onChange}
name='category'
value={category}
> >
{category} {category}
</Button> </Button>
@ -222,9 +264,11 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
{ecommerceRatings.map((rating) => ( {ecommerceRatings.map((rating) => (
<Button <Button
key={rating} key={rating}
variant={filters.ecommerceRatings.includes(rating) ? 'primary' : 'light'} variant={filters.e_commerce_level.includes(rating) ? 'primary' : 'light'}
className='rounded-pill' className='rounded-pill'
onClick={() => handleEcommerceRatingSelect(rating)} onClick={onChange}
name='e_commerce_level'
value={rating}
> >
{rating} {rating}
</Button> </Button>
@ -239,9 +283,11 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
{exposureRatings.map((rating) => ( {exposureRatings.map((rating) => (
<Button <Button
key={rating} key={rating}
variant={filters.exposureRatings.includes(rating) ? 'primary' : 'light'} variant={filters.exposure_level.includes(rating) ? 'primary' : 'light'}
className='rounded-pill' className='rounded-pill'
onClick={() => handleExposureRatingSelect(rating)} onClick={onChange}
name='exposure_level'
value={rating}
> >
{rating} {rating}
</Button> </Button>
@ -256,9 +302,11 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
{gmvRanges.map((range) => ( {gmvRanges.map((range) => (
<Button <Button
key={range.label} key={range.label}
variant={filters.gmvRanges.includes(range.label) ? 'primary' : 'light'} variant={filters.gmv_range.includes(range.label) ? 'primary' : 'light'}
className='rounded-pill' className='rounded-pill'
onClick={() => handleGmvRangeSelect(range.label)} onClick={onChange}
name='gmv_range'
value={range.label}
> >
{range.label} {range.label}
</Button> </Button>
@ -273,7 +321,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
<RangeSlider <RangeSlider
min={0} min={0}
max={500000} max={500000}
value={filters.viewsRange} value={filters.views_range}
onChange={handleViewsRangeChange} onChange={handleViewsRangeChange}
/> />
@ -313,7 +361,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
<RangeSlider <RangeSlider
min={0} min={0}
max={500000} max={500000}
value={filters.pricingRange} value={filters.pricing}
onChange={handlePricingRangeChange} 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 }) { export default function PrivateCreatorList({ path }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { privateCreators, status, selectedCreators, hasMore, isLoadingMore, pagination } = useSelector( const { privateCreators, status, selectedCreators, hasMore, isLoadingMore, pagination, error } = useSelector(
(state) => state.creators (state) => state.creators
); );
const { sortBy, sortDirection } = useSelector((state) => state.filters); const { sortBy, sortDirection } = useSelector((state) => state.filters);
@ -60,11 +60,13 @@ export default function PrivateCreatorList({ path }) {
// //
const handleSort = (field) => { const handleSort = (field) => {
return;
dispatch(setSortBy(field)); dispatch(setSortBy(field));
}; };
// //
const renderSortIcon = (field) => { const renderSortIcon = (field) => {
return;
if (sortBy === field) { if (sortBy === field) {
return sortDirection === 'asc' ? <ChevronUp size={14} /> : <ChevronDown size={14} />; 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 ( return (
<div className='text-center p-5'> <div className='text-center p-5'>
<Spinner animation='border' role='status' variant='primary'> <Spinner animation='border' role='status' variant='primary'>
@ -98,7 +100,7 @@ export default function PrivateCreatorList({ path }) {
// //
if (status === 'failed') { 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 ( return (

View File

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

View File

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