diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-08-06 21:52:21 +0200 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-08-06 21:52:21 +0200 |
| commit | ebfb7c1c8bc0a9ec50bde72eb9a0859c6e5dcee5 (patch) | |
| tree | 35353c15726d7d036907df731b00d390c1d1f538 /src/frontend/app/routes | |
| parent | 5cc27f852b02446659e0ab85305916c9f5e5a5f0 (diff) | |
Fix this fucking pile of steaming garbage
Diffstat (limited to 'src/frontend/app/routes')
| -rw-r--r-- | src/frontend/app/routes/estimates-$id.css | 5 | ||||
| -rw-r--r-- | src/frontend/app/routes/estimates-$id.tsx | 59 | ||||
| -rw-r--r-- | src/frontend/app/routes/map.css | 1 | ||||
| -rw-r--r-- | src/frontend/app/routes/settings.css | 102 | ||||
| -rw-r--r-- | src/frontend/app/routes/settings.tsx | 114 | ||||
| -rw-r--r-- | src/frontend/app/routes/stoplist.css | 5 | ||||
| -rw-r--r-- | src/frontend/app/routes/stoplist.tsx | 49 |
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> ); } |
