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.tsx54
-rw-r--r--src/frontend/app/routes/map.tsx8
-rw-r--r--src/frontend/app/routes/settings.tsx22
-rw-r--r--src/frontend/app/routes/stoplist.tsx16
-rw-r--r--src/frontend/app/routes/timetable-$id.tsx35
5 files changed, 99 insertions, 36 deletions
diff --git a/src/frontend/app/routes/estimates-$id.tsx b/src/frontend/app/routes/estimates-$id.tsx
index dc45198..c48932c 100644
--- a/src/frontend/app/routes/estimates-$id.tsx
+++ b/src/frontend/app/routes/estimates-$id.tsx
@@ -13,6 +13,7 @@ import { TimetableSkeleton } from "../components/TimetableSkeleton";
import { ErrorDisplay } from "../components/ErrorDisplay";
import { PullToRefresh } from "../components/PullToRefresh";
import { useAutoRefresh } from "../hooks/useAutoRefresh";
+import { type RegionId, getRegionConfig } from "../data/RegionConfig";
export interface StopDetails {
stop: {
@@ -35,8 +36,9 @@ interface ErrorInfo {
message?: string;
}
-const loadData = async (stopId: string): Promise<StopDetails> => {
- const resp = await fetch(`/api/GetStopEstimates?id=${stopId}`, {
+const loadData = async (region: RegionId, stopId: string): Promise<StopDetails> => {
+ const regionConfig = getRegionConfig(region);
+ const resp = await fetch(`${regionConfig.estimatesEndpoint}?id=${stopId}`, {
headers: {
Accept: "application/json",
},
@@ -49,9 +51,16 @@ const loadData = async (stopId: string): Promise<StopDetails> => {
return await resp.json();
};
-const loadTimetableData = async (stopId: string): Promise<TimetableEntry[]> => {
+const loadTimetableData = async (region: RegionId, stopId: string): Promise<TimetableEntry[]> => {
+ const regionConfig = getRegionConfig(region);
+
+ // Check if timetable is available for this region
+ if (!regionConfig.timetableEndpoint) {
+ throw new Error("Timetable not available for this region");
+ }
+
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
- const resp = await fetch(`/api/GetStopTimetable?date=${today}&stopId=${stopId}`, {
+ const resp = await fetch(`${regionConfig.timetableEndpoint}?date=${today}&stopId=${stopId}`, {
headers: {
Accept: "application/json",
},
@@ -83,7 +92,8 @@ export default function Estimates() {
const [favourited, setFavourited] = useState(false);
const [isManualRefreshing, setIsManualRefreshing] = useState(false);
- const { tableStyle } = useApp();
+ const { tableStyle, region } = useApp();
+ const regionConfig = getRegionConfig(region);
const parseError = (error: any): ErrorInfo => {
if (!navigator.onLine) {
@@ -108,10 +118,10 @@ export default function Estimates() {
setEstimatesLoading(true);
setEstimatesError(null);
- const body = await loadData(params.id!);
+ const body = await loadData(region, params.id!);
setData(body);
setDataDate(new Date());
- setCustomName(StopDataProvider.getCustomName(stopIdNum));
+ setCustomName(StopDataProvider.getCustomName(region, stopIdNum));
} catch (error) {
console.error('Error loading estimates data:', error);
setEstimatesError(parseError(error));
@@ -120,14 +130,20 @@ export default function Estimates() {
} finally {
setEstimatesLoading(false);
}
- }, [params.id, stopIdNum]);
+ }, [params.id, stopIdNum, region]);
const loadTimetableDataAsync = useCallback(async () => {
+ // Skip loading timetable if not available for this region
+ if (!regionConfig.timetableEndpoint) {
+ setTimetableLoading(false);
+ return;
+ }
+
try {
setTimetableLoading(true);
setTimetableError(null);
- const timetableBody = await loadTimetableData(params.id!);
+ const timetableBody = await loadTimetableData(region, params.id!);
setTimetableData(timetableBody);
} catch (error) {
console.error('Error loading timetable data:', error);
@@ -136,7 +152,7 @@ export default function Estimates() {
} finally {
setTimetableLoading(false);
}
- }, [params.id]);
+ }, [params.id, region, regionConfig.timetableEndpoint]);
const refreshData = useCallback(async () => {
await Promise.all([
@@ -168,16 +184,16 @@ export default function Estimates() {
loadEstimatesData();
loadTimetableDataAsync();
- StopDataProvider.pushRecent(parseInt(params.id ?? ""));
- setFavourited(StopDataProvider.isFavourite(parseInt(params.id ?? "")));
- }, [params.id, loadEstimatesData, loadTimetableDataAsync]);
+ StopDataProvider.pushRecent(region, parseInt(params.id ?? ""));
+ setFavourited(StopDataProvider.isFavourite(region, parseInt(params.id ?? "")));
+ }, [params.id, region, loadEstimatesData, loadTimetableDataAsync]);
const toggleFavourite = () => {
if (favourited) {
- StopDataProvider.removeFavourite(stopIdNum);
+ StopDataProvider.removeFavourite(region, stopIdNum);
setFavourited(false);
} else {
- StopDataProvider.addFavourite(stopIdNum);
+ StopDataProvider.addFavourite(region, stopIdNum);
setFavourited(true);
}
};
@@ -188,10 +204,10 @@ export default function Estimates() {
if (input === null) return; // cancelled
const trimmed = input.trim();
if (trimmed === "") {
- StopDataProvider.removeCustomName(stopIdNum);
+ StopDataProvider.removeCustomName(region, stopIdNum);
setCustomName(undefined);
} else {
- StopDataProvider.setCustomName(stopIdNum, trimmed);
+ StopDataProvider.setCustomName(region, stopIdNum, trimmed);
setCustomName(trimmed);
}
};
@@ -270,9 +286,9 @@ export default function Estimates() {
/>
) : data ? (
tableStyle === "grouped" ? (
- <GroupedTable data={data} dataDate={dataDate} />
+ <GroupedTable data={data} dataDate={dataDate} regionConfig={regionConfig} />
) : (
- <RegularTable data={data} dataDate={dataDate} />
+ <RegularTable data={data} dataDate={dataDate} regionConfig={regionConfig} />
)
) : null}
</div>
diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx
index 56a9c79..c3a1308 100644
--- a/src/frontend/app/routes/map.tsx
+++ b/src/frontend/app/routes/map.tsx
@@ -38,7 +38,7 @@ export default function StopMap() {
name: string;
} | null>(null);
const [isSheetOpen, setIsSheetOpen] = useState(false);
- const { mapState, updateMapState, theme } = useApp();
+ const { mapState, updateMapState, theme, region } = useApp();
const mapRef = useRef<MapRef>(null);
const [mapStyleKey, setMapStyleKey] = useState<string>("light");
@@ -56,7 +56,7 @@ export default function StopMap() {
};
useEffect(() => {
- StopDataProvider.getStops().then((data) => {
+ StopDataProvider.getStops(region).then((data) => {
const features: GeoJsonFeature<
Point,
{ stopId: number; name: string; lines: string[] }
@@ -70,7 +70,7 @@ export default function StopMap() {
}));
setStops(features);
});
- }, []);
+ }, [region]);
useEffect(() => {
//const styleName = "carto";
@@ -115,7 +115,7 @@ export default function StopMap() {
const handlePointClick = (feature: any) => {
const props: any = feature.properties;
// fetch full stop to get lines array
- StopDataProvider.getStopById(props.stopId).then((stop) => {
+ StopDataProvider.getStopById(region, props.stopId).then((stop) => {
if (!stop) return;
setSelectedStop({
stopId: stop.stopId,
diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx
index bcda311..eae6ad8 100644
--- a/src/frontend/app/routes/settings.tsx
+++ b/src/frontend/app/routes/settings.tsx
@@ -2,6 +2,7 @@ import { type Theme, useApp } from "../AppContext";
import "./settings.css";
import { useTranslation } from "react-i18next";
import { useState } from "react";
+import { getAvailableRegions } from "../data/RegionConfig";
export default function Settings() {
const { t, i18n } = useTranslation();
@@ -12,8 +13,12 @@ export default function Settings() {
setTableStyle,
mapPositionMode,
setMapPositionMode,
+ region,
+ setRegion,
} = useApp();
+ const regions = getAvailableRegions();
+
return (
<div className="page-container">
<h1 className="page-title">{t("about.title")}</h1>
@@ -21,6 +26,23 @@ export default function Settings() {
<section className="settings-section">
<h2>{t("about.settings")}</h2>
<div className="settings-content-inline">
+ <label htmlFor="region" className="form-label-inline">
+ Región:
+ </label>
+ <select
+ id="region"
+ className="form-select-inline"
+ value={region}
+ onChange={(e) => setRegion(e.target.value as any)}
+ >
+ {regions.map((r) => (
+ <option key={r.id} value={r.id}>
+ {r.name}
+ </option>
+ ))}
+ </select>
+ </div>
+ <div className="settings-content-inline">
<label htmlFor="theme" className="form-label-inline">
{t("about.theme")}
</label>
diff --git a/src/frontend/app/routes/stoplist.tsx b/src/frontend/app/routes/stoplist.tsx
index 8b0ebe2..13d3584 100644
--- a/src/frontend/app/routes/stoplist.tsx
+++ b/src/frontend/app/routes/stoplist.tsx
@@ -5,9 +5,11 @@ import StopItemSkeleton from "../components/StopItemSkeleton";
import Fuse from "fuse.js";
import "./stoplist.css";
import { useTranslation } from "react-i18next";
+import { useApp } from "../AppContext";
export default function StopList() {
const { t } = useTranslation();
+ const { region } = useApp();
const [data, setData] = useState<Stop[] | null>(null);
const [loading, setLoading] = useState(true);
const [searchResults, setSearchResults] = useState<Stop[] | null>(null);
@@ -29,19 +31,19 @@ export default function StopList() {
// Load favourite and recent IDs immediately from localStorage
useEffect(() => {
- setFavouriteIds(StopDataProvider.getFavouriteIds());
- setRecentIds(StopDataProvider.getRecent());
- }, []);
+ setFavouriteIds(StopDataProvider.getFavouriteIds(region));
+ setRecentIds(StopDataProvider.getRecent(region));
+ }, [region]);
// Load stops from network
const loadStops = useCallback(async () => {
try {
setLoading(true);
- const stops = await StopDataProvider.loadStopsFromNetwork();
+ const stops = await StopDataProvider.loadStopsFromNetwork(region);
// Add favourite flags to stops
- const favouriteStopsIds = StopDataProvider.getFavouriteIds();
+ const favouriteStopsIds = StopDataProvider.getFavouriteIds(region);
const stopsWithFavourites = stops.map(stop => ({
...stop,
favourite: favouriteStopsIds.includes(stop.stopId)
@@ -55,7 +57,7 @@ export default function StopList() {
);
setFavouriteStops(favStops);
- const recIds = StopDataProvider.getRecent();
+ const recIds = StopDataProvider.getRecent(region);
const recStops = recIds
.map(id => stopsWithFavourites.find(stop => stop.stopId === id))
.filter(Boolean) as Stop[];
@@ -66,7 +68,7 @@ export default function StopList() {
} finally {
setLoading(false);
}
- }, []);
+ }, [region]);
useEffect(() => {
loadStops();
diff --git a/src/frontend/app/routes/timetable-$id.tsx b/src/frontend/app/routes/timetable-$id.tsx
index cb55f53..1942ce8 100644
--- a/src/frontend/app/routes/timetable-$id.tsx
+++ b/src/frontend/app/routes/timetable-$id.tsx
@@ -7,6 +7,8 @@ import { TimetableSkeleton } from "../components/TimetableSkeleton";
import { ErrorDisplay } from "../components/ErrorDisplay";
import LineIcon from "../components/LineIcon";
import { useTranslation } from "react-i18next";
+import { type RegionId, getRegionConfig } from "../data/RegionConfig";
+import { useApp } from "../AppContext";
import "./timetable-$id.css";
interface ErrorInfo {
@@ -15,12 +17,19 @@ interface ErrorInfo {
message?: string;
}
-const loadTimetableData = async (stopId: string): Promise<TimetableEntry[]> => {
+const loadTimetableData = async (region: RegionId, stopId: string): Promise<TimetableEntry[]> => {
+ const regionConfig = getRegionConfig(region);
+
+ // Check if timetable is available for this region
+ if (!regionConfig.timetableEndpoint) {
+ throw new Error("Timetable not available for this region");
+ }
+
// Add delay to see skeletons in action (remove in production)
await new Promise(resolve => setTimeout(resolve, 1000));
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
- const resp = await fetch(`/api/GetStopTimetable?date=${today}&stopId=${stopId}`, {
+ const resp = await fetch(`${regionConfig.timetableEndpoint}?date=${today}&stopId=${stopId}`, {
headers: {
Accept: "application/json",
},
@@ -99,6 +108,7 @@ const parseServiceId = (serviceId: string): string => {
export default function Timetable() {
const { t } = useTranslation();
+ const { region } = useApp();
const params = useParams();
const stopIdNum = parseInt(params.id ?? "");
const [timetableData, setTimetableData] = useState<TimetableEntry[]>([]);
@@ -107,6 +117,7 @@ export default function Timetable() {
const [error, setError] = useState<ErrorInfo | null>(null);
const [showPastEntries, setShowPastEntries] = useState(false);
const nextEntryRef = useRef<HTMLDivElement>(null);
+ const regionConfig = getRegionConfig(region);
const currentTime = new Date().toTimeString().slice(0, 8); // HH:MM:SS
const filteredData = filterTimetableData(timetableData, currentTime, showPastEntries);
@@ -130,11 +141,22 @@ export default function Timetable() {
};
const loadData = async () => {
+ // Check if timetable is available for this region
+ if (!regionConfig.timetableEndpoint) {
+ setError({
+ type: 'server',
+ status: 501,
+ message: 'Timetable not available for this region'
+ });
+ setLoading(false);
+ return;
+ }
+
try {
setLoading(true);
setError(null);
- const timetableBody = await loadTimetableData(params.id!);
+ const timetableBody = await loadTimetableData(region, params.id!);
setTimetableData(timetableBody);
if (timetableBody.length > 0) {
@@ -168,8 +190,8 @@ export default function Timetable() {
useEffect(() => {
loadData();
- setCustomName(StopDataProvider.getCustomName(stopIdNum));
- }, [params.id]);
+ setCustomName(StopDataProvider.getCustomName(region, stopIdNum));
+ }, [params.id, region]);
if (loading) {
return (
@@ -266,6 +288,7 @@ const TimetableTableWithScroll: React.FC<{
nextEntryRef: React.RefObject<HTMLDivElement | null>;
}> = ({ data, showAll, currentTime, nextEntryRef }) => {
const { t } = useTranslation();
+ const { region } = useApp();
const nowMinutes = timeToMinutes(currentTime);
return (
@@ -295,7 +318,7 @@ const TimetableTableWithScroll: React.FC<{
>
<div className="card-header">
<div className="line-info">
- <LineIcon line={entry.line.name} />
+ <LineIcon line={entry.line.name} region={region} />
</div>
<div className="destination-info">