aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/routes
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/app/routes')
-rw-r--r--src/frontend/app/routes/estimates-$id.css5
-rw-r--r--src/frontend/app/routes/estimates-$id.tsx59
-rw-r--r--src/frontend/app/routes/map.css1
-rw-r--r--src/frontend/app/routes/settings.css102
-rw-r--r--src/frontend/app/routes/settings.tsx114
-rw-r--r--src/frontend/app/routes/stoplist.css5
-rw-r--r--src/frontend/app/routes/stoplist.tsx49
7 files changed, 250 insertions, 85 deletions
diff --git a/src/frontend/app/routes/estimates-$id.css b/src/frontend/app/routes/estimates-$id.css
index 424c76f..8906147 100644
--- a/src/frontend/app/routes/estimates-$id.css
+++ b/src/frontend/app/routes/estimates-$id.css
@@ -29,11 +29,6 @@
}
/* Estimates page specific styles */
-.estimates-page {
- height: 100%;
- overflow: hidden;
-}
-
.estimates-header {
display: flex;
align-items: center;
diff --git a/src/frontend/app/routes/estimates-$id.tsx b/src/frontend/app/routes/estimates-$id.tsx
index d9b9b47..1582275 100644
--- a/src/frontend/app/routes/estimates-$id.tsx
+++ b/src/frontend/app/routes/estimates-$id.tsx
@@ -8,8 +8,6 @@ import { useApp } from "../AppContext";
import { GroupedTable } from "../components/GroupedTable";
import { useTranslation } from "react-i18next";
import { TimetableTable, type TimetableEntry } from "../components/TimetableTable";
-import { usePullToRefresh } from "../hooks/usePullToRefresh";
-import { PullToRefreshIndicator } from "../components/PullToRefresh";
import { useAutoRefresh } from "../hooks/useAutoRefresh";
export interface StopDetails {
@@ -84,17 +82,6 @@ export default function Estimates() {
]);
}, [loadEstimatesData, loadTimetableDataAsync]);
- const {
- containerRef,
- isRefreshing,
- pullDistance,
- canRefresh,
- } = usePullToRefresh({
- onRefresh: refreshData,
- threshold: 80,
- enabled: true,
- });
-
// Auto-refresh estimates data every 30 seconds
useAutoRefresh({
onRefresh: loadEstimatesData,
@@ -139,23 +126,18 @@ export default function Estimates() {
return <h1 className="page-title">{t("common.loading")}</h1>;
return (
- <div ref={containerRef} className="page-container estimates-page">
- <PullToRefreshIndicator
- pullDistance={pullDistance}
- isRefreshing={isRefreshing}
- canRefresh={canRefresh}
- >
- <div className="estimates-header">
- <h1 className="page-title">
- <Star
- className={`star-icon ${favourited ? "active" : ""}`}
- onClick={toggleFavourite}
- />
- <Edit2 className="edit-icon" onClick={handleRename} />
- {customName ?? data.stop.name}{" "}
- <span className="estimates-stop-id">({data.stop.id})</span>
- </h1>
- </div>
+ <div className="page-container estimates-page">
+ <div className="estimates-header">
+ <h1 className="page-title">
+ <Star
+ className={`star-icon ${favourited ? "active" : ""}`}
+ onClick={toggleFavourite}
+ />
+ <Edit2 className="edit-icon" onClick={handleRename} />
+ {customName ?? data.stop.name}{" "}
+ <span className="estimates-stop-id">({data.stop.id})</span>
+ </h1>
+ </div>
<div className="table-responsive">
{tableStyle === "grouped" ? (
@@ -175,15 +157,14 @@ export default function Estimates() {
<div className="timetable-actions">
<Link
to={`/timetable/${params.id}`}
- className="view-all-link"
- >
- <ExternalLink className="external-icon" />
- {t("timetable.viewAll", "Ver todos los horarios")}
- </Link>
- </div>
- )}
- </div>
- </PullToRefreshIndicator>
+ className="view-all-link"
+ >
+ <ExternalLink className="external-icon" />
+ {t("timetable.viewAll", "Ver todos los horarios")}
+ </Link>
+ </div>
+ )}
+ </div>
</div>
);
}
diff --git a/src/frontend/app/routes/map.css b/src/frontend/app/routes/map.css
index 0b3ebe5..7d32de7 100644
--- a/src/frontend/app/routes/map.css
+++ b/src/frontend/app/routes/map.css
@@ -16,7 +16,6 @@
padding: 0;
margin: 0;
max-width: none;
- overflow: hidden;
}
.fullscreen-map {
diff --git a/src/frontend/app/routes/settings.css b/src/frontend/app/routes/settings.css
index 8c612d3..47de391 100644
--- a/src/frontend/app/routes/settings.css
+++ b/src/frontend/app/routes/settings.css
@@ -92,3 +92,105 @@
.settings-section p {
margin-top: 0.5em;
}
+
+/* Update controls styles */
+.update-controls {
+ display: flex;
+ gap: 1rem;
+ margin-bottom: 1rem;
+ flex-wrap: wrap;
+}
+
+.update-button,
+.clear-cache-button {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ border: none;
+ border-radius: 8px;
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-decoration: none;
+}
+
+.update-button {
+ background-color: var(--button-background-color);
+ color: white;
+}
+
+.update-button:hover:not(:disabled) {
+ background-color: var(--button-hover-background-color);
+}
+
+.update-button:disabled {
+ background-color: var(--button-disabled-background-color);
+ cursor: not-allowed;
+}
+
+.clear-cache-button {
+ background-color: #6c757d;
+ color: white;
+}
+
+.clear-cache-button:hover {
+ background-color: #5a6268;
+}
+
+.reset-pwa-button {
+ background-color: #dc3545;
+ color: white;
+ font-weight: bold;
+}
+
+.reset-pwa-button:hover {
+ background-color: #c82333;
+}
+
+.update-message {
+ padding: 0.75rem;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ margin-bottom: 1rem;
+}
+
+.update-message.success {
+ background-color: #d4edda;
+ color: #155724;
+ border: 1px solid #c3e6cb;
+}
+
+.update-message.error {
+ background-color: #f8d7da;
+ color: #721c24;
+ border: 1px solid #f5c6cb;
+}
+
+.update-help-text {
+ font-size: 0.85rem;
+ color: var(--subtitle-color);
+ line-height: 1.4;
+ margin: 0;
+}
+
+.spinning {
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+@media (max-width: 768px) {
+ .update-controls {
+ flex-direction: column;
+ }
+
+ .update-button,
+ .clear-cache-button {
+ justify-content: center;
+ }
+}
diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx
index c08b2c9..b75434d 100644
--- a/src/frontend/app/routes/settings.tsx
+++ b/src/frontend/app/routes/settings.tsx
@@ -1,6 +1,9 @@
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();
@@ -13,6 +16,66 @@ export default function Settings() {
setMapPositionMode,
} = useApp();
+ 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>
@@ -67,7 +130,7 @@ export default function Settings() {
</div>
<div className="settings-content-inline">
<label htmlFor="language" className="form-label-inline">
- Idioma:
+ {t("about.language", "Idioma")}:
</label>
<select
id="language"
@@ -90,6 +153,55 @@ 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.css b/src/frontend/app/routes/stoplist.css
index 99c2da7..253c0ab 100644
--- a/src/frontend/app/routes/stoplist.css
+++ b/src/frontend/app/routes/stoplist.css
@@ -1,9 +1,4 @@
/* Common page styles */
-.stoplist-page {
- height: 100%;
- overflow: hidden;
-}
-
.page-title {
font-size: 1.8rem;
margin-bottom: 1rem;
diff --git a/src/frontend/app/routes/stoplist.tsx b/src/frontend/app/routes/stoplist.tsx
index 70b1525..1e55dc9 100644
--- a/src/frontend/app/routes/stoplist.tsx
+++ b/src/frontend/app/routes/stoplist.tsx
@@ -4,8 +4,6 @@ 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();
@@ -27,17 +25,6 @@ export default function StopList() {
setData(stops);
}, []);
- const {
- containerRef,
- isRefreshing,
- pullDistance,
- canRefresh,
- } = usePullToRefresh({
- onRefresh: loadStops,
- threshold: 80,
- enabled: true,
- });
-
useEffect(() => {
loadStops();
}, [loadStops]);
@@ -86,21 +73,16 @@ export default function StopList() {
return <h1 className="page-title">{t("common.loading")}</h1>;
return (
- <div ref={containerRef} className="page-container stoplist-page">
- <PullToRefreshIndicator
- pullDistance={pullDistance}
- isRefreshing={isRefreshing}
- canRefresh={canRefresh}
- >
- <h1 className="page-title">UrbanoVigo Web</h1>
+ <div className="page-container stoplist-page">
+ <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"
+ <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"
@@ -156,13 +138,12 @@ export default function StopList() {
<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>
+ <ul className="list">
+ {data
+ ?.sort((a, b) => a.stopId - b.stopId)
+ .map((stop: Stop) => <StopItem key={stop.stopId} stop={stop} />)}
+ </ul>
+ </div>
</div>
);
}