Skip to content
Extraits de code Groupes Projets
Valider af980994 rédigé par oussama aftys's avatar oussama aftys
Parcourir les fichiers

added live pagination in client side

parent 5538ce1f
Branches
1 requête de fusion!226added live pagination in client side
Pipeline #18910 réussi avec l'étape
in 2 minutes et 12 secondes
export { default } from './live-filters';
\ No newline at end of file
import React from 'react';
import {
Box,
FormControl,
InputLabel,
Select,
MenuItem,
TextField,
Chip,
InputAdornment,
SelectChangeEvent
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Clear';
import { LiveStatus } from '@/shared/types/live';
interface LiveFilters {
status?: LiveStatus;
search?: string;
}
interface LiveFiltersProps {
filters: LiveFilters;
onFilterChange: (filters: Partial<LiveFilters>) => void;
onClearFilters: () => void;
isFiltered: boolean;
}
const statusLabels: Record<LiveStatus, string> = {
[LiveStatus.ONGOING]: 'En cours',
[LiveStatus.COMING]: 'À venir',
[LiveStatus.REVIEW]: 'En révision',
[LiveStatus.REPLAY]: 'Replay'
};
const LiveFiltersComponent: React.FC<LiveFiltersProps> = ({
filters,
onFilterChange,
onClearFilters,
isFiltered
}) => {
const handleStatusChange = (event: SelectChangeEvent<string>) => {
const value = event.target.value;
onFilterChange({
status: value ? (value as LiveStatus) : undefined
});
};
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
onFilterChange({
search: value || undefined
});
};
return (
<Box sx={{ mb: 3 }}>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
gap: 2,
alignItems: { xs: 'stretch', sm: 'center' },
mb: 2
}}
>
{/* Search Field */}
<TextField
placeholder="Rechercher un live..."
value={filters.search || ''}
onChange={handleSearchChange}
size="small"
sx={{ minWidth: { xs: '100%', sm: 250 } }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon color="action" />
</InputAdornment>
),
}}
/>
{/* Status Filter */}
<FormControl size="small" sx={{ minWidth: { xs: '100%', sm: 150 } }}>
<InputLabel>Statut</InputLabel>
<Select
value={filters.status || ''}
onChange={handleStatusChange}
label="Statut"
>
<MenuItem value="">
<em>Tous les statuts</em>
</MenuItem>
{Object.entries(statusLabels).map(([status, label]) => (
<MenuItem key={status} value={status}>
{label}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
{/* Active Filters Display */}
{isFiltered && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<Box component="span" sx={{ fontSize: '0.875rem', color: 'text.secondary' }}>
Filtres actifs:
</Box>
{filters.status && (
<Chip
label={`Statut: ${statusLabels[filters.status]}`}
size="small"
onDelete={() => onFilterChange({ status: undefined })}
color="primary"
variant="outlined"
/>
)}
{filters.search && (
<Chip
label={`Recherche: "${filters.search}"`}
size="small"
onDelete={() => onFilterChange({ search: undefined })}
color="primary"
variant="outlined"
/>
)}
<Chip
label="Effacer tout"
size="small"
onClick={onClearFilters}
color="default"
variant="outlined"
icon={<ClearIcon />}
/>
</Box>
)}
</Box>
);
};
export default LiveFiltersComponent;
\ No newline at end of file
export { default } from './pagination';
\ No newline at end of file
import React from 'react';
import { Box, Pagination as MuiPagination, Typography } from '@mui/material';
interface PaginationProps {
currentPage: number;
totalPages: number;
totalElements: number;
pageSize: number;
onPageChange: (page: number) => void;
loading?: boolean;
}
const Pagination: React.FC<PaginationProps> = ({
currentPage,
totalPages,
totalElements,
pageSize,
onPageChange,
loading = false
}) => {
const handlePageChange = (event: React.ChangeEvent<unknown>, page: number) => {
if (!loading) {
onPageChange(page - 1); // MUI Pagination est 1-based, mais notre API est 0-based
}
};
const startItem = currentPage * pageSize + 1;
const endItem = Math.min((currentPage + 1) * pageSize, totalElements);
if (totalPages <= 1) {
return null;
}
return (
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
alignItems: 'center',
justifyContent: 'space-between',
gap: 2,
mt: 4,
mb: 2
}}
>
<Typography variant="body2" color="text.secondary">
Affichage {startItem}-{endItem} sur {totalElements} lives
</Typography>
<MuiPagination
count={totalPages}
page={currentPage + 1} // MUI Pagination est 1-based
onChange={handlePageChange}
color="primary"
size="medium"
disabled={loading}
showFirstButton
showLastButton
sx={{
'& .MuiPaginationItem-root': {
fontSize: '0.875rem',
}
}}
/>
</Box>
);
};
export default Pagination;
\ No newline at end of file
import { useState, useCallback, useMemo } from 'react';
import { useGetLivesPaginated } from '@/shared/api/live';
import { LiveStatus } from '@/shared/types/live';
interface UseLivePaginationOptions {
initialPageSize?: number;
initialPage?: number;
}
interface LiveFilters {
status?: LiveStatus;
search?: string;
}
export const useLivePagination = (options: UseLivePaginationOptions = {}) => {
const { initialPageSize = 8, initialPage = 0 } = options;
const [currentPage, setCurrentPage] = useState(initialPage);
const [pageSize, setPageSize] = useState(initialPageSize);
const [filters, setFilters] = useState<LiveFilters>({});
const {
livesData,
livesLoading,
livesError,
totalElements,
totalPages,
hasNext,
hasPrevious,
livesValidating
} = useGetLivesPaginated(currentPage, pageSize);
// Filtrer les données côté client (à améliorer côté serveur plus tard)
const filteredLivesData = useMemo(() => {
if (!livesData) return [];
let filtered = [...livesData];
if (filters.status) {
filtered = filtered.filter(live => live.status === filters.status);
}
if (filters.search) {
const searchLower = filters.search.toLowerCase();
filtered = filtered.filter(live =>
live.title.toLowerCase().includes(searchLower) ||
live.description.toLowerCase().includes(searchLower)
);
}
return filtered;
}, [livesData, filters]);
const handlePageChange = useCallback((page: number) => {
setCurrentPage(page);
}, []);
const handlePageSizeChange = useCallback((newPageSize: number) => {
setPageSize(newPageSize);
setCurrentPage(0); // Reset to first page
}, []);
const handleFilterChange = useCallback((newFilters: Partial<LiveFilters>) => {
setFilters(prev => ({ ...prev, ...newFilters }));
setCurrentPage(0); // Reset to first page when filters change
}, []);
const clearFilters = useCallback(() => {
setFilters({});
setCurrentPage(0);
}, []);
const goToPage = useCallback((page: number) => {
if (page >= 0 && page < totalPages) {
setCurrentPage(page);
}
}, [totalPages]);
const goToNextPage = useCallback(() => {
if (hasNext) {
setCurrentPage(prev => prev + 1);
}
}, [hasNext]);
const goToPreviousPage = useCallback(() => {
if (hasPrevious) {
setCurrentPage(prev => prev - 1);
}
}, [hasPrevious]);
const scrollToTop = useCallback(() => {
window.scrollTo({ top: 0, behavior: 'smooth' });
}, []);
return {
// Data
livesData: filteredLivesData,
totalElements,
totalPages,
currentPage,
pageSize,
hasNext,
hasPrevious,
// Loading states
livesLoading,
livesError,
livesValidating,
// Filters
filters,
// Actions
handlePageChange,
handlePageSizeChange,
handleFilterChange,
clearFilters,
goToPage,
goToNextPage,
goToPreviousPage,
scrollToTop,
// Computed
isEmpty: filteredLivesData.length === 0,
isFiltered: Object.keys(filters).some(key => filters[key as keyof LiveFilters]),
};
};
\ No newline at end of file
"use client";
import React, { useEffect } from 'react';
import { Grid, Box, Typography, Fade } from '@mui/material';
import LiveCard from '../all-lives/live-card';
import CircularProgress from "@mui/material/CircularProgress";
import FlexRowCenter from "@/components/flex-box/flex-row-center";
import { useSearchParams } from 'next/navigation';
import Pagination from '@/components/pagination';
import LiveFilters from '@/components/live-filters';
import { useLivePagination } from '@/hooks/use-live-pagination';
const LIVES_PER_PAGE = 8;
const AllLivesAdvanced = () => {
const params = useSearchParams();
const liveId = params.get('liveId');
const {
// Data
livesData,
totalElements,
totalPages,
currentPage,
pageSize,
// Loading states
livesLoading,
livesError,
// Filters
filters,
// Actions
handlePageChange,
handleFilterChange,
clearFilters,
scrollToTop,
// Computed
isEmpty,
isFiltered,
} = useLivePagination({
initialPageSize: LIVES_PER_PAGE,
initialPage: 0
});
const handlePageChangeWithScroll = (page: number) => {
handlePageChange(page);
scrollToTop();
};
if (livesError) {
return (
<Box sx={{ paddingX: { xs: 3, md: 10, lg: 20 }, paddingY: 4 }}>
<Box sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h6" color="error">
Erreur lors du chargement des lives
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
Veuillez rafraîchir la page ou réessayer plus tard
</Typography>
</Box>
</Box>
);
}
return (
<Box sx={{ paddingX: { xs: 3, md: 10, lg: 20 }, paddingY: 4 }}>
{/* Header section */}
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography variant="h4" component="h1" sx={{ mb: 2, fontWeight: 'bold' }}>
Nos Lives
</Typography>
<Typography variant="subtitle1" color="text.secondary">
Découvrez nos livestreams et interagissez en temps réel
</Typography>
</Box>
{/* Filters */}
<LiveFilters
filters={filters}
onFilterChange={handleFilterChange}
onClearFilters={clearFilters}
isFiltered={isFiltered}
/>
{/* Loading State */}
{livesLoading && (
<FlexRowCenter minHeight="400px">
<CircularProgress color="primary" />
</FlexRowCenter>
)}
{/* Content */}
{!livesLoading && (
<Fade in={!livesLoading}>
<div>
{/* Lives Grid */}
<Grid container spacing={3}>
{isEmpty ? (
<Grid item xs={12}>
<Box sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h6" color="text.secondary">
{isFiltered
? 'Aucun live ne correspond à vos critères'
: 'Aucun live disponible pour le moment'
}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{isFiltered
? 'Essayez de modifier vos filtres ou effacez-les pour voir tous les lives'
: 'Revenez plus tard pour découvrir nos nouveaux lives !'
}
</Typography>
</Box>
</Grid>
) : (
livesData.map((live) => (
<Grid item xs={12} sm={6} lg={6} key={live.id}>
<LiveCard
open={liveId === live.id}
title={live.title}
description={live.description}
status={live.status}
id={live.id}
date={new Date(live.startDate)}
imageUrl={live.image}
/>
</Grid>
))
)}
</Grid>
{/* Pagination */}
{totalElements > 0 && !isEmpty && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
totalElements={totalElements}
pageSize={pageSize}
onPageChange={handlePageChangeWithScroll}
loading={livesLoading}
/>
)}
</div>
</Fade>
)}
</Box>
);
};
export default AllLivesAdvanced;
\ No newline at end of file
"use client"; "use client";
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { Grid, Card, CardMedia, CardContent, Typography, Button } from '@mui/material'; import { Grid, Card, CardMedia, CardContent, Typography, Button, Box } from '@mui/material';
import LiveCard from '../all-lives/live-card'; import LiveCard from '../all-lives/live-card';
import { useGetLives } from '@/shared/api/live'; import { useGetLivesPaginated } from '@/shared/api/live';
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import FlexRowCenter from "@/components/flex-box/flex-row-center"; import FlexRowCenter from "@/components/flex-box/flex-row-center";
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import Pagination from '@/components/pagination';
// import LiveNowComponent from '../LiveNowComponent'; // import LiveNowComponent from '../LiveNowComponent';
const LIVES_PER_PAGE = 8;
const LivePage = () => { const LivePage = () => {
const { livesData, livesLoading } = useGetLives(); const [currentPage, setCurrentPage] = useState(0);
const params = useSearchParams() const {
livesData,
livesLoading,
totalElements,
totalPages,
pageSize,
hasNext,
hasPrevious
} = useGetLivesPaginated(currentPage, LIVES_PER_PAGE);
const params = useSearchParams();
const liveId = params.get('liveId'); const liveId = params.get('liveId');
useEffect(() => {
}, [livesData]); const handlePageChange = (page: number) => {
setCurrentPage(page);
// Scroll to top when changing page
window.scrollTo({ top: 0, behavior: 'smooth' });
};
return ( useEffect(() => {
<>{livesLoading ? (<FlexRowCenter minHeight="100vh"> // Reset to first page when component mounts
<CircularProgress color="primary" /> setCurrentPage(0);
</FlexRowCenter>) : ( }, []);
<Grid container spacing={2} sx={{ paddingX: { xs: 3, md: 10, lg: 20 }, paddingY: 20 }}>
{/* <LiveNowComponent /> */} return (
<>
{livesLoading ? (
<FlexRowCenter minHeight="100vh">
<CircularProgress color="primary" />
</FlexRowCenter>
) : (
<Box sx={{ paddingX: { xs: 3, md: 10, lg: 20 }, paddingY: 4 }}>
{/* Header section */}
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography variant="h4" component="h1" sx={{ mb: 2, fontWeight: 'bold' }}>
Nos Lives
</Typography>
<Typography variant="subtitle1" color="text.secondary">
Découvrez nos livestreams et interagissez en temps réel
</Typography>
</Box>
{livesData.map((live, index) => ( {/* Lives Grid */}
<Grid item xs={12} md={6} key={index}> <Grid container spacing={3}>
<LiveCard open={liveId == live.id} title={live.title} description={live.description} status={live.status} id={live.id} date={new Date(live.startDate)} imageUrl={live.image} /> {/* <LiveNowComponent /> */}
{livesData.length === 0 ? (
<Grid item xs={12}>
<Box sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h6" color="text.secondary">
Aucun live disponible pour le moment
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
Revenez plus tard pour découvrir nos nouveaux lives !
</Typography>
</Box>
</Grid>
) : (
livesData.map((live, index) => (
<Grid item xs={12} sm={6} lg={6} key={live.id}>
<LiveCard
open={liveId === live.id}
title={live.title}
description={live.description}
status={live.status}
id={live.id}
date={new Date(live.startDate)}
imageUrl={live.image}
/>
</Grid>
))
)}
</Grid> </Grid>
))}
</Grid>
)} {/* Pagination */}
{totalElements > 0 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
totalElements={totalElements}
pageSize={pageSize}
onPageChange={handlePageChange}
loading={livesLoading}
/>
)}
</Box>
)}
</> </>
); );
}; };
......
export { default as AllLivesView } from "./all-lives"; export { default as AllLivesView } from "./all-lives";
export { default as AllLivesAdvancedView } from "./all-lives-advanced";
export { default as LiveView } from "./live-popup"; export { default as LiveView } from "./live-popup";
...@@ -41,6 +41,30 @@ export function useGetLives() { ...@@ -41,6 +41,30 @@ export function useGetLives() {
return memoizedValue; return memoizedValue;
} }
export function useGetLivesPaginated(page: number = 0, size: number = 10) {
const URL = [endpoints.live.clientWithPagination(page, size)];
const { data, isLoading, error, isValidating } = useSWR(URL, fetcher, options);
const memoizedValue = useMemo(
() => ({
livesData: sortLivesByStatus((data?.content as ILiveItem[]) || []),
totalElements: data?.totalElements || 0,
totalPages: data?.totalPages || 0,
currentPage: data?.number || 0,
pageSize: data?.size || size,
hasNext: !data?.last || false,
hasPrevious: !data?.first || false,
livesLoading: isLoading,
livesError: error,
livesValidating: isValidating,
}),
[data, error, isLoading, isValidating, size]
);
return memoizedValue;
}
export function useGetLive(liveId: number | string) { export function useGetLive(liveId: number | string) {
......
...@@ -97,6 +97,7 @@ export const endpoints = { ...@@ -97,6 +97,7 @@ export const endpoints = {
}, },
live: { live: {
client: '/api/stream/live/client', client: '/api/stream/live/client',
clientWithPagination: (page: number, size: number) => `/api/stream/live/client/paginated?page=${page}&size=${size}`,
get: (liveId: string) => `/api/stream/live?liveId=${liveId}`, get: (liveId: string) => `/api/stream/live?liveId=${liveId}`,
ongoing: '/api/stream/live/ongoing', ongoing: '/api/stream/live/ongoing',
......
0% ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter