aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/public
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/public
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/public')
-rw-r--r--src/frontend/public/manifest.webmanifest2
-rw-r--r--src/frontend/public/sw.js133
2 files changed, 119 insertions, 16 deletions
diff --git a/src/frontend/public/manifest.webmanifest b/src/frontend/public/manifest.webmanifest
index 5dd08fa..619dfc6 100644
--- a/src/frontend/public/manifest.webmanifest
+++ b/src/frontend/public/manifest.webmanifest
@@ -10,6 +10,8 @@
"lang": "es",
"background_color": "#ffffff",
"theme_color": "#007bff",
+ "categories": ["travel", "utilities", "productivity"],
+ "prefer_related_applications": false,
"icons": [
{
"src": "/logo-512.jpg",
diff --git a/src/frontend/public/sw.js b/src/frontend/public/sw.js
index 01d403a..ca826f6 100644
--- a/src/frontend/public/sw.js
+++ b/src/frontend/public/sw.js
@@ -1,52 +1,153 @@
-const API_CACHE_NAME = "api-cache-v1";
-const API_URL_PATTERN = /\/api\/(GetStopList)/;
+const CACHE_VERSION = "v2";
+const API_CACHE_NAME = `api-cache-${CACHE_VERSION}`;
+const STATIC_CACHE_NAME = `static-cache-${CACHE_VERSION}`;
+const API_URL_PATTERN = /\/api\/(GetStopList|GetStopEstimates|GetStopTimetable)/;
const API_MAX_AGE = 24 * 60 * 60 * 1000; // 24 hours
+const ESTIMATES_MIN_AGE = 15 * 1000; // 15 seconds minimum
+const ESTIMATES_MAX_AGE = 30 * 1000; // 30 seconds maximum
self.addEventListener("install", (event) => {
- event.waitUntil(self.skipWaiting());
+ console.log("SW: Installing new version");
+ event.waitUntil(
+ caches.open(STATIC_CACHE_NAME).then((cache) => {
+ return cache.addAll([
+ "/",
+ "/manifest.webmanifest",
+ "/stops.json",
+ "/favicon.ico",
+ "/logo-256.png",
+ "/logo-512.jpg"
+ ]);
+ }).then(() => {
+ return self.skipWaiting();
+ })
+ );
});
self.addEventListener("activate", (event) => {
- event.waitUntil(self.clients.claim());
+ console.log("SW: Activating new version");
+ event.waitUntil(
+ Promise.all([
+ // Clean up old caches
+ caches.keys().then((cacheNames) => {
+ return Promise.all(
+ cacheNames.map((cacheName) => {
+ if (cacheName !== API_CACHE_NAME && cacheName !== STATIC_CACHE_NAME) {
+ console.log("SW: Deleting old cache:", cacheName);
+ return caches.delete(cacheName);
+ }
+ })
+ );
+ }),
+ // Take control of all clients
+ self.clients.claim(),
+ // Notify clients about the update
+ self.clients.matchAll().then((clients) => {
+ clients.forEach((client) => {
+ client.postMessage({ type: "SW_UPDATED" });
+ });
+ })
+ ])
+ );
});
self.addEventListener("fetch", async (event) => {
const url = new URL(event.request.url);
- if (event.request.method !== "GET" || !API_URL_PATTERN.test(url.pathname)) {
+ // Handle API requests with caching
+ if (event.request.method === "GET" && API_URL_PATTERN.test(url.pathname)) {
+ event.respondWith(handleApiRequest(event.request));
return;
}
- event.respondWith(apiCacheFirst(event.request));
+ // Handle static assets
+ if (event.request.method === "GET") {
+ event.respondWith(handleStaticRequest(event.request));
+ return;
+ }
});
-async function apiCacheFirst(request) {
+async function handleApiRequest(request) {
+ const url = new URL(request.url);
+ const isEstimates = url.pathname.includes("GetStopEstimates");
+ // Random cache age between 15-30 seconds for estimates to prevent thundering herd
+ const maxAge = isEstimates
+ ? ESTIMATES_MIN_AGE + Math.random() * (ESTIMATES_MAX_AGE - ESTIMATES_MIN_AGE)
+ : API_MAX_AGE;
+
const cache = await caches.open(API_CACHE_NAME);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
- const age =
- Date.now() - new Date(cachedResponse.headers.get("date")).getTime();
- if (age < API_MAX_AGE) {
+ const age = Date.now() - new Date(cachedResponse.headers.get("date")).getTime();
+ if (age < maxAge) {
console.debug(`SW: Cache HIT for ${request.url}`);
return cachedResponse;
}
-
// Cache is too old, fetch a fresh copy
cache.delete(request);
}
try {
const netResponse = await fetch(request);
+
+ if (netResponse.ok) {
+ const responseToCache = netResponse.clone();
+ cache.put(request, responseToCache);
+ console.debug(`SW: Cache MISS for ${request.url}`);
+ }
- const responseToCache = netResponse.clone();
-
- cache.put(request, responseToCache);
-
- console.debug(`SW: Cache MISS for ${request.url}`);
+ return netResponse;
+ } catch (error) {
+ // If network fails and we have a cached response (even if old), return it
+ if (cachedResponse) {
+ console.debug(`SW: Network failed, returning stale cache for ${request.url}`);
+ return cachedResponse;
+ }
+ throw error;
+ }
+}
+async function handleStaticRequest(request) {
+ const cache = await caches.open(STATIC_CACHE_NAME);
+ const cachedResponse = await cache.match(request);
+
+ if (cachedResponse) {
+ return cachedResponse;
+ }
+
+ try {
+ const netResponse = await fetch(request);
+ if (netResponse.ok) {
+ cache.put(request, netResponse.clone());
+ }
return netResponse;
} catch (error) {
+ // Return a basic offline page for navigation requests
+ if (request.mode === 'navigate') {
+ return new Response('App is offline', {
+ status: 503,
+ statusText: 'Service Unavailable',
+ headers: { 'Content-Type': 'text/plain' }
+ });
+ }
throw error;
}
}
+
+// Handle messages from the main thread
+self.addEventListener("message", (event) => {
+ if (event.data && event.data.type === "SKIP_WAITING") {
+ self.skipWaiting();
+ }
+
+ if (event.data && event.data.type === "CLEAR_CACHE") {
+ event.waitUntil(
+ caches.keys().then((cacheNames) => {
+ return Promise.all(
+ cacheNames.map((cacheName) => caches.delete(cacheName))
+ );
+ })
+ );
+ }
+});