From 5cc27f852b02446659e0ab85305916c9f5e5a5f0 Mon Sep 17 00:00:00 2001
From: Ariel Costas Guerrero <ariel@costas.dev>
Date: Wed, 6 Aug 2025 00:12:19 +0200
Subject: 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.
---
 src/frontend/app/routes/stoplist.tsx | 148 ++++++++++++++++++++---------------
 1 file changed, 86 insertions(+), 62 deletions(-)

(limited to 'src/frontend/app/routes/stoplist.tsx')

diff --git a/src/frontend/app/routes/stoplist.tsx b/src/frontend/app/routes/stoplist.tsx
index 58cdab4..70b1525 100644
--- a/src/frontend/app/routes/stoplist.tsx
+++ b/src/frontend/app/routes/stoplist.tsx
@@ -1,9 +1,11 @@
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState, useCallback } from "react";
 import StopDataProvider, { type Stop } from "../data/StopDataProvider";
 import StopItem from "../components/StopItem";
 import Fuse from "fuse.js";
 import "./stoplist.css";
 import { useTranslation } from "react-i18next";
+import { usePullToRefresh } from "../hooks/usePullToRefresh";
+import { PullToRefreshIndicator } from "../components/PullToRefresh";
 
 export default function StopList() {
   const { t } = useTranslation();
@@ -20,10 +22,26 @@ export default function StopList() {
     [data],
   );
 
-  useEffect(() => {
-    StopDataProvider.getStops().then((stops: Stop[]) => setData(stops));
+  const loadStops = useCallback(async () => {
+    const stops = await StopDataProvider.getStops();
+    setData(stops);
   }, []);
 
+  const {
+    containerRef,
+    isRefreshing,
+    pullDistance,
+    canRefresh,
+  } = usePullToRefresh({
+    onRefresh: loadStops,
+    threshold: 80,
+    enabled: true,
+  });
+
+  useEffect(() => {
+    loadStops();
+  }, [loadStops]);
+
   const handleStopSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
     const stopName = event.target.value || "";
 
@@ -68,77 +86,83 @@ export default function StopList() {
     return <h1 className="page-title">{t("common.loading")}</h1>;
 
   return (
-    <div className="page-container">
-      <h1 className="page-title">UrbanoVigo Web</h1>
-
-      <form className="search-form">
-        <div className="form-group">
-          <label className="form-label" htmlFor="stopName">
-            {t("stoplist.search_label", "Buscar paradas")}
-          </label>
-          <input
-            className="form-input"
-            type="text"
-            placeholder={randomPlaceholder}
-            id="stopName"
-            onChange={handleStopSearch}
-          />
-        </div>
-      </form>
+    <div ref={containerRef} className="page-container stoplist-page">
+      <PullToRefreshIndicator
+        pullDistance={pullDistance}
+        isRefreshing={isRefreshing}
+        canRefresh={canRefresh}
+      >
+        <h1 className="page-title">UrbanoVigo Web</h1>
+
+        <form className="search-form">
+          <div className="form-group">
+            <label className="form-label" htmlFor="stopName">
+              {t("stoplist.search_label", "Buscar paradas")}
+            </label>
+            <input
+              className="form-input"
+              type="text"
+              placeholder={randomPlaceholder}
+              id="stopName"
+              onChange={handleStopSearch}
+            />
+          </div>
+        </form>
+
+        {searchResults && searchResults.length > 0 && (
+          <div className="list-container">
+            <h2 className="page-subtitle">
+              {t("stoplist.search_results", "Resultados de la búsqueda")}
+            </h2>
+            <ul className="list">
+              {searchResults.map((stop: Stop) => (
+                <StopItem key={stop.stopId} stop={stop} />
+              ))}
+            </ul>
+          </div>
+        )}
 
-      {searchResults && searchResults.length > 0 && (
         <div className="list-container">
-          <h2 className="page-subtitle">
-            {t("stoplist.search_results", "Resultados de la búsqueda")}
-          </h2>
+          <h2 className="page-subtitle">{t("stoplist.favourites")}</h2>
+
+          {favouritedStops?.length === 0 && (
+            <p className="message">
+              {t(
+                "stoplist.no_favourites",
+                "Accede a una parada y márcala como favorita para verla aquí.",
+              )}
+            </p>
+          )}
+
           <ul className="list">
-            {searchResults.map((stop: Stop) => (
-              <StopItem key={stop.stopId} stop={stop} />
-            ))}
+            {favouritedStops
+              ?.sort((a, b) => a.stopId - b.stopId)
+              .map((stop: Stop) => <StopItem key={stop.stopId} stop={stop} />)}
           </ul>
         </div>
-      )}
-
-      <div className="list-container">
-        <h2 className="page-subtitle">{t("stoplist.favourites")}</h2>
-
-        {favouritedStops?.length === 0 && (
-          <p className="message">
-            {t(
-              "stoplist.no_favourites",
-              "Accede a una parada y márcala como favorita para verla aquí.",
-            )}
-          </p>
-        )}
 
-        <ul className="list">
-          {favouritedStops
-            ?.sort((a, b) => a.stopId - b.stopId)
-            .map((stop: Stop) => <StopItem key={stop.stopId} stop={stop} />)}
-        </ul>
-      </div>
+        {recentStops && recentStops.length > 0 && (
+          <div className="list-container">
+            <h2 className="page-subtitle">{t("stoplist.recents")}</h2>
+
+            <ul className="list">
+              {recentStops.map((stop: Stop) => (
+                <StopItem key={stop.stopId} stop={stop} />
+              ))}
+            </ul>
+          </div>
+        )}
 
-      {recentStops && recentStops.length > 0 && (
         <div className="list-container">
-          <h2 className="page-subtitle">{t("stoplist.recents")}</h2>
+          <h2 className="page-subtitle">{t("stoplist.all_stops", "Paradas")}</h2>
 
           <ul className="list">
-            {recentStops.map((stop: Stop) => (
-              <StopItem key={stop.stopId} stop={stop} />
-            ))}
+            {data
+              ?.sort((a, b) => a.stopId - b.stopId)
+              .map((stop: Stop) => <StopItem key={stop.stopId} stop={stop} />)}
           </ul>
         </div>
-      )}
-
-      <div className="list-container">
-        <h2 className="page-subtitle">{t("stoplist.all_stops", "Paradas")}</h2>
-
-        <ul className="list">
-          {data
-            ?.sort((a, b) => a.stopId - b.stopId)
-            .map((stop: Stop) => <StopItem key={stop.stopId} stop={stop} />)}
-        </ul>
-      </div>
+      </PullToRefreshIndicator>
     </div>
   );
 }
-- 
cgit v1.3

