aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/hooks
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-08-06 00:12:19 +0200
committerAriel Costas Guerrero <ariel@costas.dev>2025-08-06 00:12:19 +0200
commit5cc27f852b02446659e0ab85305916c9f5e5a5f0 (patch)
tree622636a2a7eade5442a3efb1726d822657d30295 /src/frontend/app/hooks
parentb04fd7d33d07f9eddea2eb53e1389d5ca5453413 (diff)
feat: Implement pull-to-refresh functionality across various components
- Added `PullToRefresh` component to enable pull-to-refresh behavior in `StopList` and `Estimates` pages. - Integrated `usePullToRefresh` hook to manage pull-to-refresh state and actions. - Created `UpdateNotification` component to inform users of available updates from the service worker. - Enhanced service worker management with `ServiceWorkerManager` class for better update handling and caching strategies. - Updated CSS styles for new components and improved layout for better user experience. - Refactored API caching logic in service worker to handle multiple endpoints and dynamic cache expiration. - Added auto-refresh functionality for estimates data to keep information up-to-date.
Diffstat (limited to 'src/frontend/app/hooks')
-rw-r--r--src/frontend/app/hooks/useAutoRefresh.ts63
-rw-r--r--src/frontend/app/hooks/usePullToRefresh.ts99
2 files changed, 162 insertions, 0 deletions
diff --git a/src/frontend/app/hooks/useAutoRefresh.ts b/src/frontend/app/hooks/useAutoRefresh.ts
new file mode 100644
index 0000000..172fa94
--- /dev/null
+++ b/src/frontend/app/hooks/useAutoRefresh.ts
@@ -0,0 +1,63 @@
+import { useEffect, useRef, useCallback } from "react";
+
+interface UseAutoRefreshOptions {
+ onRefresh: () => Promise<void>;
+ interval?: number;
+ enabled?: boolean;
+}
+
+export function useAutoRefresh({
+ onRefresh,
+ interval = 30000, // 30 seconds default
+ enabled = true,
+}: UseAutoRefreshOptions) {
+ const intervalRef = useRef<NodeJS.Timeout | null>(null);
+ const refreshCallbackRef = useRef(onRefresh);
+
+ // Update callback ref when it changes
+ useEffect(() => {
+ refreshCallbackRef.current = onRefresh;
+ }, [onRefresh]);
+
+ const startAutoRefresh = useCallback(() => {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ }
+
+ if (enabled) {
+ intervalRef.current = setInterval(() => {
+ refreshCallbackRef.current();
+ }, interval);
+ }
+ }, [interval, enabled]);
+
+ const stopAutoRefresh = useCallback(() => {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ intervalRef.current = null;
+ }
+ }, []);
+
+ useEffect(() => {
+ startAutoRefresh();
+ return stopAutoRefresh;
+ }, [startAutoRefresh, stopAutoRefresh]);
+
+ // Handle visibility change to pause/resume auto-refresh
+ useEffect(() => {
+ const handleVisibilityChange = () => {
+ if (document.hidden) {
+ stopAutoRefresh();
+ } else {
+ startAutoRefresh();
+ }
+ };
+
+ document.addEventListener("visibilitychange", handleVisibilityChange);
+ return () => {
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
+ };
+ }, [startAutoRefresh, stopAutoRefresh]);
+
+ return { startAutoRefresh, stopAutoRefresh };
+}
diff --git a/src/frontend/app/hooks/usePullToRefresh.ts b/src/frontend/app/hooks/usePullToRefresh.ts
new file mode 100644
index 0000000..b34502b
--- /dev/null
+++ b/src/frontend/app/hooks/usePullToRefresh.ts
@@ -0,0 +1,99 @@
+import { useEffect, useRef, useState } from "react";
+
+interface PullToRefreshOptions {
+ onRefresh: () => Promise<void>;
+ threshold?: number;
+ resistance?: number;
+ enabled?: boolean;
+}
+
+export function usePullToRefresh({
+ onRefresh,
+ threshold = 80,
+ resistance = 2.5,
+ enabled = true,
+}: PullToRefreshOptions) {
+ const containerRef = useRef<HTMLDivElement>(null);
+ const [isRefreshing, setIsRefreshing] = useState(false);
+ const [pullDistance, setPullDistance] = useState(0);
+ const [isDragging, setIsDragging] = useState(false);
+
+ const startY = useRef(0);
+ const currentY = useRef(0);
+ const isAtTop = useRef(true);
+
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container || !enabled) return;
+
+ let rafId: number;
+
+ const checkScrollPosition = () => {
+ isAtTop.current = container.scrollTop <= 5;
+ };
+
+ const handleTouchStart = (e: TouchEvent) => {
+ if (!isAtTop.current || isRefreshing) return;
+
+ startY.current = e.touches[0].clientY;
+ currentY.current = startY.current;
+ setIsDragging(true);
+ };
+
+ const handleTouchMove = (e: TouchEvent) => {
+ if (!isDragging || isRefreshing || !isAtTop.current) return;
+
+ currentY.current = e.touches[0].clientY;
+ const deltaY = currentY.current - startY.current;
+
+ if (deltaY > 0) {
+ e.preventDefault();
+ const distance = Math.min(deltaY / resistance, threshold * 1.5);
+ setPullDistance(distance);
+ }
+ };
+
+ const handleTouchEnd = async () => {
+ if (!isDragging || isRefreshing) return;
+
+ setIsDragging(false);
+
+ if (pullDistance >= threshold) {
+ setIsRefreshing(true);
+ try {
+ await onRefresh();
+ } finally {
+ setIsRefreshing(false);
+ }
+ }
+
+ setPullDistance(0);
+ };
+
+ const handleScroll = () => {
+ if (rafId) cancelAnimationFrame(rafId);
+ rafId = requestAnimationFrame(checkScrollPosition);
+ };
+
+ container.addEventListener("touchstart", handleTouchStart, { passive: false });
+ container.addEventListener("touchmove", handleTouchMove, { passive: false });
+ container.addEventListener("touchend", handleTouchEnd);
+ container.addEventListener("scroll", handleScroll, { passive: true });
+
+ return () => {
+ if (rafId) cancelAnimationFrame(rafId);
+ container.removeEventListener("touchstart", handleTouchStart);
+ container.removeEventListener("touchmove", handleTouchMove);
+ container.removeEventListener("touchend", handleTouchEnd);
+ container.removeEventListener("scroll", handleScroll);
+ };
+ }, [enabled, isRefreshing, pullDistance, threshold, resistance, onRefresh, isDragging]);
+
+ return {
+ containerRef,
+ isRefreshing,
+ pullDistance,
+ isDragging,
+ canRefresh: pullDistance >= threshold,
+ };
+}