aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/app')
-rw-r--r--src/frontend/app/components/TimetableTable.tsx18
-rw-r--r--src/frontend/app/components/UpdateNotification.tsx63
-rw-r--r--src/frontend/app/root.tsx32
-rw-r--r--src/frontend/app/routes/estimates-$id.tsx35
-rw-r--r--src/frontend/app/routes/settings.tsx108
-rw-r--r--src/frontend/app/routes/stoplist.tsx91
-rw-r--r--src/frontend/app/utils/serviceWorkerManager.ts155
7 files changed, 80 insertions, 422 deletions
diff --git a/src/frontend/app/components/TimetableTable.tsx b/src/frontend/app/components/TimetableTable.tsx
index d03ddf4..86896ca 100644
--- a/src/frontend/app/components/TimetableTable.tsx
+++ b/src/frontend/app/components/TimetableTable.tsx
@@ -144,16 +144,14 @@ export const TimetableTable: React.FC<TimetableTableProps> = ({
</div>
</div>
<div className="card-body">
- {!isPast && (
- <div className="route-streets">
- <span className="service-id">
- {parseServiceId(entry.trip.service_id)}
- </span>
- {entry.next_streets.length > 0 && (
- <span> — {entry.next_streets.join(' — ')}</span>
- )}
- </div>
- )}
+ <div className="route-streets">
+ <span className="service-id">
+ {parseServiceId(entry.trip.service_id)}
+ </span>
+ {entry.next_streets.length > 0 && (
+ <span> — {entry.next_streets.join(' — ')}</span>
+ )}
+ </div>
</div>
</div>
);
diff --git a/src/frontend/app/components/UpdateNotification.tsx b/src/frontend/app/components/UpdateNotification.tsx
deleted file mode 100644
index c8e21ea..0000000
--- a/src/frontend/app/components/UpdateNotification.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { useState, useEffect } from "react";
-import { Download, X } from "lucide-react";
-import { swManager } from "../utils/serviceWorkerManager";
-import "./UpdateNotification.css";
-
-export function UpdateNotification() {
- const [showUpdate, setShowUpdate] = useState(false);
- const [isUpdating, setIsUpdating] = useState(false);
-
- useEffect(() => {
- swManager.onUpdate(() => {
- setShowUpdate(true);
- });
- }, []);
-
- const handleUpdate = async () => {
- setIsUpdating(true);
- swManager.activateUpdate();
-
- // Wait for the page to reload
- setTimeout(() => {
- window.location.reload();
- }, 500);
- };
-
- const handleDismiss = () => {
- setShowUpdate(false);
- };
-
- if (!showUpdate) return null;
-
- return (
- <div className="update-notification">
- <div className="update-content">
- <div className="update-icon">
- <Download size={20} />
- </div>
- <div className="update-text">
- <div className="update-title">Nueva versión disponible</div>
- <div className="update-description">
- Actualiza para obtener las últimas mejoras
- </div>
- </div>
- <div className="update-actions">
- <button
- className="update-button"
- onClick={handleUpdate}
- disabled={isUpdating}
- >
- {isUpdating ? "Actualizando..." : "Actualizar"}
- </button>
- <button
- className="update-dismiss"
- onClick={handleDismiss}
- aria-label="Cerrar notificación"
- >
- <X size={16} />
- </button>
- </div>
- </div>
- </div>
- );
-}
diff --git a/src/frontend/app/root.tsx b/src/frontend/app/root.tsx
index 040494f..25a873f 100644
--- a/src/frontend/app/root.tsx
+++ b/src/frontend/app/root.tsx
@@ -18,9 +18,6 @@ import "maplibre-theme/modern.css";
import { Protocol } from "pmtiles";
import maplibregl, { type LngLatLike } from "maplibre-gl";
import { AppProvider } from "./AppContext";
-import { swManager } from "./utils/serviceWorkerManager";
-import { UpdateNotification } from "./components/UpdateNotification";
-import { useEffect } from "react";
const pmtiles = new Protocol();
maplibregl.addProtocol("pmtiles", pmtiles.tile);
//#endregion
@@ -30,7 +27,7 @@ import "./i18n";
export const links: Route.LinksFunction = () => [];
export function HydrateFallback() {
- return "Loading...";
+ return "Cargando...";
}
export function Layout({ children }: { children: React.ReactNode }) {
@@ -90,32 +87,19 @@ export function Layout({ children }: { children: React.ReactNode }) {
);
}
-// Helper: check if coordinates are within Vigo bounds
-function isWithinVigo(lngLat: LngLatLike): boolean {
- let lng: number, lat: number;
- if (Array.isArray(lngLat)) {
- [lng, lat] = lngLat;
- } else if ("lng" in lngLat && "lat" in lngLat) {
- lng = lngLat.lng;
- lat = lngLat.lat;
- } else {
- return false;
- }
- // Rough bounding box for Vigo
- return lat >= 42.18 && lat <= 42.3 && lng >= -8.78 && lng <= -8.65;
-}
-
import NavBar from "./components/NavBar";
export default function App() {
- useEffect(() => {
- // Initialize service worker
- swManager.initialize();
- }, []);
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker
+ .register('/pwa-worker.js')
+ .catch((error) => {
+ console.error('Error registering SW:', error);
+ });
+ }
return (
<AppProvider>
- <UpdateNotification />
<main className="main-content">
<Outlet />
</main>
diff --git a/src/frontend/app/routes/estimates-$id.tsx b/src/frontend/app/routes/estimates-$id.tsx
index 1582275..ab10c53 100644
--- a/src/frontend/app/routes/estimates-$id.tsx
+++ b/src/frontend/app/routes/estimates-$id.tsx
@@ -122,8 +122,9 @@ export default function Estimates() {
}
};
- if (data === null)
+ if (data === null) {
return <h1 className="page-title">{t("common.loading")}</h1>;
+ }
return (
<div className="page-container estimates-page">
@@ -139,24 +140,24 @@ export default function Estimates() {
</h1>
</div>
- <div className="table-responsive">
- {tableStyle === "grouped" ? (
- <GroupedTable data={data} dataDate={dataDate} />
- ) : (
- <RegularTable data={data} dataDate={dataDate} />
- )}
- </div>
+ <div className="table-responsive">
+ {tableStyle === "grouped" ? (
+ <GroupedTable data={data} dataDate={dataDate} />
+ ) : (
+ <RegularTable data={data} dataDate={dataDate} />
+ )}
+ </div>
- <div className="timetable-section">
- <TimetableTable
- data={timetableData}
- currentTime={new Date().toTimeString().slice(0, 8)} // HH:MM:SS
- />
+ <div className="timetable-section">
+ <TimetableTable
+ data={timetableData}
+ currentTime={new Date().toTimeString().slice(0, 8)} // HH:MM:SS
+ />
- {timetableData.length > 0 && (
- <div className="timetable-actions">
- <Link
- to={`/timetable/${params.id}`}
+ {timetableData.length > 0 && (
+ <div className="timetable-actions">
+ <Link
+ to={`/timetable/${params.id}`}
className="view-all-link"
>
<ExternalLink className="external-icon" />
diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx
index b75434d..3bc3492 100644
--- a/src/frontend/app/routes/settings.tsx
+++ b/src/frontend/app/routes/settings.tsx
@@ -2,8 +2,6 @@ import { useApp } from "../AppContext";
import "./settings.css";
import { useTranslation } from "react-i18next";
import { useState } from "react";
-import { swManager } from "../utils/serviceWorkerManager";
-import { RotateCcw, Download } from "lucide-react";
export default function Settings() {
const { t, i18n } = useTranslation();
@@ -19,63 +17,6 @@ export default function Settings() {
const [isCheckingUpdates, setIsCheckingUpdates] = useState(false);
const [updateMessage, setUpdateMessage] = useState<string | null>(null);
- const handleCheckForUpdates = async () => {
- setIsCheckingUpdates(true);
- setUpdateMessage(null);
-
- try {
- // Check if service worker is supported
- if (!("serviceWorker" in navigator)) {
- setUpdateMessage(t("about.sw_not_supported", "Service Workers no son compatibles en este navegador"));
- return;
- }
-
- // Force check for updates
- await swManager.checkForUpdates();
-
- // Wait a moment for the update check to complete
- setTimeout(() => {
- if (swManager.isUpdateAvailable()) {
- setUpdateMessage(t("about.update_available", "¡Nueva versión disponible! Aparecerá una notificación para actualizar."));
- } else {
- setUpdateMessage(t("about.up_to_date", "Ya tienes la versión más reciente."));
- }
- }, 2000);
-
- } catch (error) {
- console.error("Error checking for updates:", error);
- setUpdateMessage(t("about.update_error", "Error al comprobar actualizaciones. Intenta recargar la página."));
- } finally {
- setIsCheckingUpdates(false);
- }
- };
-
- const handleClearCache = async () => {
- if (confirm(t("about.clear_cache_confirm", "¿Estás seguro de que quieres limpiar la caché? Esto eliminará todos los datos guardados localmente."))) {
- try {
- await swManager.clearCache();
- setUpdateMessage(t("about.cache_cleared", "Caché limpiada. La página se recargará para aplicar los cambios."));
- setTimeout(() => {
- window.location.reload();
- }, 2000);
- } catch (error) {
- console.error("Error clearing cache:", error);
- setUpdateMessage(t("about.cache_error", "Error al limpiar la caché."));
- }
- }
- };
-
- const handleResetPWA = async () => {
- if (confirm(t("about.reset_pwa_confirm", "¿Estás seguro? Esto eliminará TODOS los datos de la aplicación y la reiniciará completamente. Úsalo solo si hay problemas graves de caché."))) {
- try {
- await swManager.resetPWA();
- } catch (error) {
- console.error("Error resetting PWA:", error);
- setUpdateMessage(t("about.reset_pwa_error", "Error al reiniciar la PWA."));
- }
- }
- };
-
return (
<div className="page-container">
<h1 className="page-title">{t("about.title")}</h1>
@@ -153,55 +94,6 @@ export default function Settings() {
<dd>{t("about.details_grouped")}</dd>
</dl>
</details>
-
- <div className="settings-section">
- <h3>{t("about.app_updates", "Actualizaciones de la aplicación")}</h3>
- <div className="update-controls">
- <button
- className="update-button"
- onClick={handleCheckForUpdates}
- disabled={isCheckingUpdates}
- >
- {isCheckingUpdates ? (
- <>
- <RotateCcw className="spinning" size={18} />
- {t("about.checking_updates", "Comprobando...")}
- </>
- ) : (
- <>
- <Download size={18} />
- {t("about.check_updates", "Comprobar actualizaciones")}
- </>
- )}
- </button>
-
- <button
- className="clear-cache-button"
- onClick={handleClearCache}
- >
- <RotateCcw size={18} />
- {t("about.clear_cache", "Limpiar caché")}
- </button>
-
- <button
- className="reset-pwa-button"
- onClick={handleResetPWA}
- >
- <RotateCcw size={18} />
- {t("about.reset_pwa", "Reiniciar PWA (Nuclear)")}
- </button>
- </div>
-
- {updateMessage && (
- <div className={`update-message ${updateMessage.includes("Error") || updateMessage.includes("error") ? 'error' : 'success'}`}>
- {updateMessage}
- </div>
- )}
-
- <p className="update-help-text">
- {t("about.update_help", "Si tienes problemas con la aplicación o no ves las últimas funciones, usa estos botones para forzar una actualización o limpiar los datos guardados.")}
- </p>
- </div>
</section>
<h2>{t("about.credits")}</h2>
<p>
diff --git a/src/frontend/app/routes/stoplist.tsx b/src/frontend/app/routes/stoplist.tsx
index 1e55dc9..885a0da 100644
--- a/src/frontend/app/routes/stoplist.tsx
+++ b/src/frontend/app/routes/stoplist.tsx
@@ -69,8 +69,9 @@ export default function StopList() {
return stopsList.reverse();
}, [data]);
- if (data === null)
+ if (data === null) {
return <h1 className="page-title">{t("common.loading")}</h1>;
+ }
return (
<div className="page-container stoplist-page">
@@ -83,60 +84,60 @@ export default function StopList() {
</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>
- )}
+ type="text"
+ placeholder={randomPlaceholder}
+ id="stopName"
+ onChange={handleStopSearch}
+ />
+ </div>
+ </form>
+ {searchResults && searchResults.length > 0 && (
<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>
- )}
-
+ <h2 className="page-subtitle">
+ {t("stoplist.search_results", "Resultados de la búsqueda")}
+ </h2>
<ul className="list">
- {favouritedStops
- ?.sort((a, b) => a.stopId - b.stopId)
- .map((stop: Stop) => <StopItem key={stop.stopId} stop={stop} />)}
+ {searchResults.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>
+ <div className="list-container">
+ <h2 className="page-subtitle">{t("stoplist.favourites")}</h2>
- <ul className="list">
- {recentStops.map((stop: Stop) => (
- <StopItem key={stop.stopId} stop={stop} />
- ))}
- </ul>
- </div>
+ {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.all_stops", "Paradas")}</h2>
+ <h2 className="page-subtitle">{t("stoplist.recents")}</h2>
+
+ <ul className="list">
+ {recentStops.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
diff --git a/src/frontend/app/utils/serviceWorkerManager.ts b/src/frontend/app/utils/serviceWorkerManager.ts
deleted file mode 100644
index a1ddbab..0000000
--- a/src/frontend/app/utils/serviceWorkerManager.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-export class ServiceWorkerManager {
- private registration: ServiceWorkerRegistration | null = null;
- private updateAvailable = false;
- private onUpdateCallback?: () => void;
-
- async initialize() {
- if (!("serviceWorker" in navigator)) {
- console.log("Service Workers not supported");
- return;
- }
-
- try {
- // First, unregister any old service workers to start fresh
- const registrations = await navigator.serviceWorker.getRegistrations();
- for (const registration of registrations) {
- if (registration.scope.includes(window.location.origin)) {
- console.log("Unregistering old service worker:", registration.scope);
- await registration.unregister();
- }
- }
-
- // Register the new worker with a fresh name
- this.registration = await navigator.serviceWorker.register("/pwa-worker.js", {
- updateViaCache: 'none' // Disable caching for the SW file itself
- });
- console.log("PWA Worker registered with scope:", this.registration.scope);
-
- // Implement proper updatefound detection (web.dev pattern)
- await this.detectSWUpdate();
-
- // Check for updates periodically
- setInterval(() => {
- this.checkForUpdates();
- }, 30 * 1000);
-
- // Check when page becomes visible
- document.addEventListener("visibilitychange", () => {
- if (!document.hidden) {
- this.checkForUpdates();
- }
- });
-
- } catch (error) {
- console.error("Service Worker registration failed:", error);
- }
- }
-
- private async detectSWUpdate() {
- if (!this.registration) return;
-
- // Listen for new service worker discovery
- this.registration.addEventListener("updatefound", () => {
- const newSW = this.registration!.installing;
- if (!newSW) return;
-
- console.log("New service worker found, monitoring installation...");
-
- newSW.addEventListener("statechange", () => {
- console.log("New SW state:", newSW.state);
-
- if (newSW.state === "installed") {
- if (navigator.serviceWorker.controller) {
- // New service worker is installed, but old one is still controlling
- // This means an update is available
- console.log("New service worker installed - update available!");
- this.updateAvailable = true;
- this.onUpdateCallback?.();
- } else {
- // First install, no controller yet
- console.log("Service worker installed for the first time");
- }
- }
-
- if (newSW.state === "activated") {
- console.log("New service worker activated");
- // Optionally notify about successful update
- }
- });
- });
-
- // Also listen for controller changes
- navigator.serviceWorker.addEventListener("controllerchange", () => {
- console.log("Service worker controller changed - reloading page");
- window.location.reload();
- });
- }
-
- async checkForUpdates() {
- if (this.registration) {
- try {
- await this.registration.update();
- } catch (error) {
- console.error("Failed to check for updates:", error);
- }
- }
- }
-
- activateUpdate() {
- if (this.registration && this.registration.waiting) {
- this.registration.waiting.postMessage({ type: "SKIP_WAITING" });
- this.updateAvailable = false;
- }
- }
-
- onUpdate(callback: () => void) {
- this.onUpdateCallback = callback;
- }
-
- isUpdateAvailable() {
- return this.updateAvailable;
- }
-
- async clearCache(): Promise<void> {
- try {
- // Delete all caches
- const cacheNames = await caches.keys();
- await Promise.all(cacheNames.map(name => caches.delete(name)));
- console.log("All caches cleared");
- } catch (error) {
- console.error("Failed to clear cache:", error);
- throw error;
- }
- }
-
- // Nuclear option: completely reset the PWA
- async resetPWA(): Promise<void> {
- try {
- console.log("Resetting PWA completely...");
-
- // 1. Unregister ALL service workers
- const registrations = await navigator.serviceWorker.getRegistrations();
- await Promise.all(registrations.map(reg => reg.unregister()));
-
- // 2. Clear all caches
- await this.clearCache();
-
- // 3. Clear local storage (optional)
- localStorage.clear();
- sessionStorage.clear();
-
- console.log("PWA reset complete - reloading...");
-
- // 4. Force reload after a short delay
- setTimeout(() => {
- window.location.reload();
- }, 500);
-
- } catch (error) {
- console.error("Failed to reset PWA:", error);
- throw error;
- }
- }
-}
-
-export const swManager = new ServiceWorkerManager();