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.tsx139
-rw-r--r--src/frontend/app/routes/stoplist.css5
-rw-r--r--src/frontend/app/routes/stoplist.tsx142
-rw-r--r--src/frontend/app/routes/timetable-$id.css8
5 files changed, 185 insertions, 114 deletions
diff --git a/src/frontend/app/routes/estimates-$id.css b/src/frontend/app/routes/estimates-$id.css
index 8906147..424c76f 100644
--- a/src/frontend/app/routes/estimates-$id.css
+++ b/src/frontend/app/routes/estimates-$id.css
@@ -29,6 +29,11 @@
}
/* 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 b5ae91a..d9b9b47 100644
--- a/src/frontend/app/routes/estimates-$id.tsx
+++ b/src/frontend/app/routes/estimates-$id.tsx
@@ -1,4 +1,4 @@
-import { type JSX, useEffect, useState } from "react";
+import { type JSX, useEffect, useState, useCallback } from "react";
import { useParams, Link } from "react-router";
import StopDataProvider from "../data/StopDataProvider";
import { Star, Edit2, ExternalLink } from "lucide-react";
@@ -8,6 +8,9 @@ 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 {
stop: {
@@ -62,23 +65,51 @@ export default function Estimates() {
const [timetableData, setTimetableData] = useState<TimetableEntry[]>([]);
const { tableStyle } = useApp();
- useEffect(() => {
- // Load real-time estimates
- loadData(params.id!).then((body: StopDetails) => {
- setData(body);
- setDataDate(new Date());
- setCustomName(StopDataProvider.getCustomName(stopIdNum));
- });
+ const loadEstimatesData = useCallback(async () => {
+ const body: StopDetails = await loadData(params.id!);
+ setData(body);
+ setDataDate(new Date());
+ setCustomName(StopDataProvider.getCustomName(stopIdNum));
+ }, [params.id, stopIdNum]);
- // Load timetable data
- loadTimetableData(params.id!).then((timetableBody: TimetableEntry[]) => {
- setTimetableData(timetableBody);
- });
+ const loadTimetableDataAsync = useCallback(async () => {
+ const timetableBody: TimetableEntry[] = await loadTimetableData(params.id!);
+ setTimetableData(timetableBody);
+ }, [params.id]);
- StopDataProvider.pushRecent(parseInt(params.id ?? ""));
+ const refreshData = useCallback(async () => {
+ await Promise.all([
+ loadEstimatesData(),
+ loadTimetableDataAsync()
+ ]);
+ }, [loadEstimatesData, loadTimetableDataAsync]);
+
+ const {
+ containerRef,
+ isRefreshing,
+ pullDistance,
+ canRefresh,
+ } = usePullToRefresh({
+ onRefresh: refreshData,
+ threshold: 80,
+ enabled: true,
+ });
+
+ // Auto-refresh estimates data every 30 seconds
+ useAutoRefresh({
+ onRefresh: loadEstimatesData,
+ interval: 30000,
+ enabled: true,
+ });
+
+ useEffect(() => {
+ // Initial load
+ loadEstimatesData();
+ loadTimetableDataAsync();
+ StopDataProvider.pushRecent(parseInt(params.id ?? ""));
setFavourited(StopDataProvider.isFavourite(parseInt(params.id ?? "")));
- }, [params.id]);
+ }, [params.id, loadEstimatesData, loadTimetableDataAsync]);
const toggleFavourite = () => {
if (favourited) {
@@ -108,45 +139,51 @@ export default function Estimates() {
return <h1 className="page-title">{t("common.loading")}</h1>;
return (
- <div className="page-container">
- <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 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="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}`}
- className="view-all-link"
- >
- <ExternalLink className="external-icon" />
- {t("timetable.viewAll", "Ver todos los horarios")}
- </Link>
- </div>
- )}
- </div>
+ {timetableData.length > 0 && (
+ <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>
</div>
);
}
diff --git a/src/frontend/app/routes/stoplist.css b/src/frontend/app/routes/stoplist.css
index 253c0ab..99c2da7 100644
--- a/src/frontend/app/routes/stoplist.css
+++ b/src/frontend/app/routes/stoplist.css
@@ -1,4 +1,9 @@
/* 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 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>
+ <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>
+ <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>
+ {recentStops && recentStops.length > 0 && (
+ <div className="list-container">
+ <h2 className="page-subtitle">{t("stoplist.recents")}</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">
+ {recentStops.map((stop: Stop) => (
+ <StopItem key={stop.stopId} stop={stop} />
+ ))}
+ </ul>
+ </div>
)}
- <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>
+ <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>
);
}
diff --git a/src/frontend/app/routes/timetable-$id.css b/src/frontend/app/routes/timetable-$id.css
index 5ae472c..5296615 100644
--- a/src/frontend/app/routes/timetable-$id.css
+++ b/src/frontend/app/routes/timetable-$id.css
@@ -62,7 +62,7 @@
}
.timetable-controls {
- margin-bottom: 1.5rem;
+ margin-bottom: 1.5rem;
display: flex;
justify-content: center;
}
@@ -124,15 +124,15 @@
.page-title {
font-size: 1.5rem;
}
-
+
.page-title .stop-name {
font-size: 1.1rem;
}
-
+
.timetable-full-content .timetable-cards {
gap: 0.75rem;
}
-
+
.timetable-full-content .timetable-card {
padding: 1rem;
}