feat: implement pull-to-refresh functionality across ClientLibraryPage, ClientSeriesPage, and ClientHomePage for improved user experience
This commit is contained in:
83
src/components/common/PullToRefreshIndicator.tsx
Normal file
83
src/components/common/PullToRefreshIndicator.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface PullToRefreshIndicatorProps {
|
||||
isPulling: boolean;
|
||||
isRefreshing: boolean;
|
||||
progress: number;
|
||||
canRefresh: boolean;
|
||||
isHiding: boolean;
|
||||
}
|
||||
|
||||
export function PullToRefreshIndicator({
|
||||
isPulling,
|
||||
isRefreshing,
|
||||
progress,
|
||||
canRefresh,
|
||||
isHiding,
|
||||
}: PullToRefreshIndicatorProps) {
|
||||
if (!isPulling && !isRefreshing && !isHiding) return null;
|
||||
|
||||
const rotation = progress * 180;
|
||||
const barWidth = Math.min(progress * 200, 200); // Barre de 200px max
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed top-0 left-1/2 transform -translate-x-1/2 z-50 transition-all",
|
||||
isHiding ? "duration-300 ease-out" : "duration-200",
|
||||
(isPulling || isRefreshing) && !isHiding ? "translate-y-0 opacity-100" : "-translate-y-full opacity-0"
|
||||
)}
|
||||
style={{
|
||||
transform: `translate(-50%, ${(isPulling || isRefreshing) && !isHiding ? (isRefreshing ? 60 : progress * 60) : -100}px)`,
|
||||
}}
|
||||
>
|
||||
{/* Barre de fond */}
|
||||
<div className="w-48 h-1 bg-muted rounded-full overflow-hidden">
|
||||
{/* Barre de progression */}
|
||||
<div
|
||||
className={cn(
|
||||
"h-full transition-all duration-200 rounded-full",
|
||||
(canRefresh || isRefreshing) ? "bg-primary" : "bg-muted-foreground"
|
||||
)}
|
||||
style={{
|
||||
width: `${isRefreshing ? 200 : barWidth}px`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Icône centrée */}
|
||||
<div className="flex justify-center mt-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center w-8 h-8 rounded-full transition-all duration-200",
|
||||
(canRefresh || isRefreshing) ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<RefreshCw
|
||||
className={cn(
|
||||
"h-4 w-4 transition-all duration-200",
|
||||
isRefreshing && "animate-spin"
|
||||
)}
|
||||
style={{
|
||||
transform: isRefreshing ? "rotate(0deg)" : `rotate(${rotation}deg)`,
|
||||
animationDuration: isRefreshing ? "2s" : undefined,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div
|
||||
className={cn(
|
||||
"mt-2 text-center text-xs transition-opacity duration-200",
|
||||
(canRefresh || isRefreshing) ? "text-primary opacity-100" : "text-muted-foreground opacity-70"
|
||||
)}
|
||||
>
|
||||
{isRefreshing ? "Actualisation..." : canRefresh ? "Relâchez pour actualiser" : "Tirez pour actualiser"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { useRouter } from "next/navigation";
|
||||
import { HomeContent } from "./HomeContent";
|
||||
import { ErrorMessage } from "@/components/ui/ErrorMessage";
|
||||
import { HomePageSkeleton } from "@/components/skeletons/OptimizedSkeletons";
|
||||
import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator";
|
||||
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import type { HomeData } from "@/types/home";
|
||||
|
||||
@@ -81,6 +83,13 @@ export function ClientHomePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const pullToRefresh = usePullToRefresh({
|
||||
onRefresh: async () => {
|
||||
await handleRefresh();
|
||||
},
|
||||
enabled: !loading && !error && !!data,
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <HomePageSkeleton />;
|
||||
}
|
||||
@@ -105,6 +114,17 @@ export function ClientHomePage() {
|
||||
);
|
||||
}
|
||||
|
||||
return <HomeContent data={data} refreshHome={handleRefresh} />;
|
||||
return (
|
||||
<>
|
||||
<PullToRefreshIndicator
|
||||
isPulling={pullToRefresh.isPulling}
|
||||
isRefreshing={pullToRefresh.isRefreshing}
|
||||
progress={pullToRefresh.progress}
|
||||
canRefresh={pullToRefresh.canRefresh}
|
||||
isHiding={pullToRefresh.isHiding}
|
||||
/>
|
||||
<HomeContent data={data} refreshHome={handleRefresh} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Library, BookOpen } from "lucide-react";
|
||||
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
||||
import { RefreshButton } from "./RefreshButton";
|
||||
@@ -17,13 +18,18 @@ interface LibraryHeaderProps {
|
||||
export const LibraryHeader = ({ library, seriesCount, series, refreshLibrary }: LibraryHeaderProps) => {
|
||||
const { t } = useTranslate();
|
||||
|
||||
// Sélectionner une série aléatoire pour l'image centrale
|
||||
const randomSeries = series.length > 0 ? series[Math.floor(Math.random() * series.length)] : null;
|
||||
|
||||
// Sélectionner une autre série aléatoire pour le fond (différente de celle du centre)
|
||||
const backgroundSeries = series.length > 1
|
||||
? series.filter(s => s.id !== randomSeries?.id)[Math.floor(Math.random() * (series.length - 1))]
|
||||
: randomSeries;
|
||||
// Mémoriser la sélection des séries pour éviter les rerenders inutiles
|
||||
const { randomSeries, backgroundSeries } = useMemo(() => {
|
||||
// Sélectionner une série aléatoire pour l'image centrale
|
||||
const random = series.length > 0 ? series[Math.floor(Math.random() * series.length)] : null;
|
||||
|
||||
// Sélectionner une autre série aléatoire pour le fond (différente de celle du centre)
|
||||
const background = series.length > 1
|
||||
? series.filter(s => s.id !== random?.id)[Math.floor(Math.random() * (series.length - 1))]
|
||||
: random;
|
||||
|
||||
return { randomSeries: random, backgroundSeries: background };
|
||||
}, [series]);
|
||||
|
||||
return (
|
||||
<div className="relative min-h-[200px] md:h-[200px] w-screen -ml-[calc((100vw-100%)/2)] overflow-hidden">
|
||||
|
||||
Reference in New Issue
Block a user