feat: implement pull-to-refresh functionality across ClientLibraryPage, ClientSeriesPage, and ClientHomePage for improved user experience
This commit is contained in:
183
src/hooks/usePullToRefresh.ts
Normal file
183
src/hooks/usePullToRefresh.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface UsePullToRefreshOptions {
|
||||
onRefresh: () => Promise<void>;
|
||||
threshold?: number;
|
||||
resistance?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface PullToRefreshState {
|
||||
isPulling: boolean;
|
||||
isRefreshing: boolean;
|
||||
pullDistance: number;
|
||||
canRefresh: boolean;
|
||||
isHiding: boolean;
|
||||
}
|
||||
|
||||
export function usePullToRefresh({
|
||||
onRefresh,
|
||||
threshold = 80,
|
||||
resistance = 0.5,
|
||||
enabled = true,
|
||||
}: UsePullToRefreshOptions) {
|
||||
const [state, setState] = useState<PullToRefreshState>({
|
||||
isPulling: false,
|
||||
isRefreshing: false,
|
||||
pullDistance: 0,
|
||||
canRefresh: false,
|
||||
isHiding: false,
|
||||
});
|
||||
|
||||
const startY = useRef(0);
|
||||
const currentY = useRef(0);
|
||||
const startScrollTop = useRef(0);
|
||||
const isValidPull = useRef(false);
|
||||
const isRefreshingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
if (isRefreshingRef.current) return;
|
||||
|
||||
// Ignorer les touches sur les éléments interactifs (boutons, liens, menu, etc.)
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target.closest('button') ||
|
||||
target.closest('a') ||
|
||||
target.closest('[role="button"]') ||
|
||||
target.closest('nav') ||
|
||||
target.closest('header') ||
|
||||
target.closest('[data-no-pull-refresh]')
|
||||
) {
|
||||
isValidPull.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
startScrollTop.current = scrollTop;
|
||||
|
||||
// Ne démarrer que si on est vraiment en haut de la page
|
||||
if (scrollTop === 0) {
|
||||
startY.current = e.touches[0].clientY;
|
||||
currentY.current = e.touches[0].clientY;
|
||||
isValidPull.current = true;
|
||||
} else {
|
||||
isValidPull.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: TouchEvent) => {
|
||||
if (!isValidPull.current || isRefreshingRef.current) return;
|
||||
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
currentY.current = e.touches[0].clientY;
|
||||
const deltaY = currentY.current - startY.current;
|
||||
|
||||
// Vérifier qu'on est toujours en haut ET qu'on tire vers le bas
|
||||
if (scrollTop === 0 && deltaY > 0) {
|
||||
const pullDistance = Math.min(deltaY * resistance, threshold * 1.5);
|
||||
const canRefresh = pullDistance >= threshold;
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isPulling: true,
|
||||
pullDistance,
|
||||
canRefresh,
|
||||
}));
|
||||
|
||||
// Empêcher le scroll par défaut quand on tire vers le bas
|
||||
if (pullDistance > 10) {
|
||||
e.preventDefault();
|
||||
}
|
||||
} else if (scrollTop > 0 || deltaY < 0) {
|
||||
// Si on scrolle ou qu'on tire vers le haut, annuler
|
||||
isValidPull.current = false;
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isPulling: false,
|
||||
pullDistance: 0,
|
||||
canRefresh: false,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = async () => {
|
||||
if (!isValidPull.current || isRefreshingRef.current) {
|
||||
isValidPull.current = false;
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isPulling: false,
|
||||
pullDistance: 0,
|
||||
canRefresh: false,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldRefresh = state.canRefresh;
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isPulling: false,
|
||||
}));
|
||||
|
||||
if (shouldRefresh) {
|
||||
isRefreshingRef.current = true;
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isRefreshing: true,
|
||||
pullDistance: 0,
|
||||
}));
|
||||
|
||||
try {
|
||||
await onRefresh();
|
||||
} catch (error) {
|
||||
console.error("Pull to refresh error:", error);
|
||||
} finally {
|
||||
isRefreshingRef.current = false;
|
||||
// Activer l'animation de disparition
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isHiding: true,
|
||||
}));
|
||||
|
||||
// Attendre la fin de l'animation avant de masquer complètement
|
||||
setTimeout(() => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isRefreshing: false,
|
||||
isHiding: false,
|
||||
}));
|
||||
}, 300); // Durée de l'animation en ms
|
||||
}
|
||||
} else {
|
||||
// Animation de retour
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
pullDistance: 0,
|
||||
canRefresh: false,
|
||||
}));
|
||||
}
|
||||
|
||||
isValidPull.current = false;
|
||||
};
|
||||
|
||||
document.addEventListener("touchstart", handleTouchStart, { passive: false });
|
||||
document.addEventListener("touchmove", handleTouchMove, { passive: false });
|
||||
document.addEventListener("touchend", handleTouchEnd);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("touchstart", handleTouchStart);
|
||||
document.removeEventListener("touchmove", handleTouchMove);
|
||||
document.removeEventListener("touchend", handleTouchEnd);
|
||||
};
|
||||
}, [state.isPulling, state.canRefresh, onRefresh, threshold, resistance, enabled]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
progress: Math.min(state.pullDistance / threshold, 1),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user