aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/app')
-rw-r--r--src/frontend/app/AppContext.tsx2
-rw-r--r--src/frontend/app/components/LineIcon.tsx27
-rw-r--r--src/frontend/app/components/PlannerOverlay.tsx3
-rw-r--r--src/frontend/app/components/RegionSelector.tsx33
-rw-r--r--src/frontend/app/components/StopMapModal.tsx6
-rw-r--r--src/frontend/app/components/map/StopSummarySheet.css (renamed from src/frontend/app/components/StopSummarySheet.css)0
-rw-r--r--src/frontend/app/components/map/StopSummarySheet.tsx (renamed from src/frontend/app/components/StopSummarySheet.tsx)51
-rw-r--r--src/frontend/app/components/map/StopSummarySheetSkeleton.tsx (renamed from src/frontend/app/components/StopSummarySheetSkeleton.tsx)0
-rw-r--r--src/frontend/app/config/RegionConfig.ts44
-rw-r--r--src/frontend/app/config/constants.ts22
-rw-r--r--src/frontend/app/contexts/MapContext.tsx10
-rw-r--r--src/frontend/app/contexts/SettingsContext.tsx2
-rw-r--r--src/frontend/app/data/SpecialPlacesProvider.ts6
-rw-r--r--src/frontend/app/data/StopDataProvider.ts64
-rw-r--r--src/frontend/app/routes/home.tsx2
-rw-r--r--src/frontend/app/routes/map.tsx142
-rw-r--r--src/frontend/app/routes/planner.tsx10
-rw-r--r--src/frontend/app/routes/stops-$id.tsx7
18 files changed, 196 insertions, 235 deletions
diff --git a/src/frontend/app/AppContext.tsx b/src/frontend/app/AppContext.tsx
index 2102ad7..8ff7de2 100644
--- a/src/frontend/app/AppContext.tsx
+++ b/src/frontend/app/AppContext.tsx
@@ -1,6 +1,6 @@
/* eslint-disable react-refresh/only-export-components */
import { type ReactNode } from "react";
-import { type RegionId } from "./config/RegionConfig";
+import { type RegionId } from "./config/constants";
import { MapProvider, useMap } from "./contexts/MapContext";
import {
SettingsProvider,
diff --git a/src/frontend/app/components/LineIcon.tsx b/src/frontend/app/components/LineIcon.tsx
index 8bbeb20..5d85c60 100644
--- a/src/frontend/app/components/LineIcon.tsx
+++ b/src/frontend/app/components/LineIcon.tsx
@@ -4,9 +4,16 @@ import "./LineIcon.css";
interface LineIconProps {
line: string;
mode?: "rounded" | "pill" | "default";
+ colour?: string;
+ textColour?: string;
}
-const LineIcon: React.FC<LineIconProps> = ({ line, mode = "default" }) => {
+const LineIcon: React.FC<LineIconProps> = ({
+ line,
+ mode = "default",
+ colour,
+ textColour,
+}) => {
const actualLine = useMemo(() => {
return line.trim().replace("510", "NAD");
}, [line]);
@@ -15,16 +22,26 @@ const LineIcon: React.FC<LineIconProps> = ({ line, mode = "default" }) => {
return /^[a-zA-Z]/.test(actualLine) ? actualLine : `L${actualLine}`;
}, [actualLine]);
- const cssVarName = `--line-${formattedLine.toLowerCase()}`;
- const cssTextVarName = `--line-${formattedLine.toLowerCase()}-text`;
+ const actualLineColour = useMemo(() => {
+ const actualColour = colour?.startsWith("#") ? colour : `#${colour}`;
+ return colour ? actualColour : `var(--line-${formattedLine.toLowerCase()})`;
+ }, [formattedLine]);
+ const actualTextColour = useMemo(() => {
+ const actualTextColour = textColour?.startsWith("#")
+ ? textColour
+ : `#${textColour}`;
+ return textColour
+ ? actualTextColour
+ : `var(--line-${formattedLine.toLowerCase()}-text, #000000)`;
+ }, [formattedLine]);
return (
<span
className={`line-icon-${mode}`}
style={
{
- "--line-colour": `var(${cssVarName})`,
- "--line-text-colour": `var(${cssTextVarName}, #000000)`,
+ "--line-colour": actualLineColour,
+ "--line-text-colour": actualTextColour,
} as React.CSSProperties
}
>
diff --git a/src/frontend/app/components/PlannerOverlay.tsx b/src/frontend/app/components/PlannerOverlay.tsx
index 12cfb0f..af71e48 100644
--- a/src/frontend/app/components/PlannerOverlay.tsx
+++ b/src/frontend/app/components/PlannerOverlay.tsx
@@ -8,7 +8,6 @@ import React, {
} from "react";
import { useTranslation } from "react-i18next";
import PlaceListItem from "~/components/PlaceListItem";
-import { REGION_DATA } from "~/config/RegionConfig";
import {
reverseGeocode,
searchPlaces,
@@ -59,7 +58,7 @@ export const PlannerOverlay: React.FC<PlannerOverlayProps> = ({
[]
);
const [recentPlaces, setRecentPlaces] = useState<PlannerSearchResult[]>([]);
- const RECENT_KEY = `recentPlaces_${REGION_DATA.id}`;
+ const RECENT_KEY = `recentPlaces`;
const clearRecentPlaces = useCallback(() => {
setRecentPlaces([]);
try {
diff --git a/src/frontend/app/components/RegionSelector.tsx b/src/frontend/app/components/RegionSelector.tsx
deleted file mode 100644
index 124b574..0000000
--- a/src/frontend/app/components/RegionSelector.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { useApp } from "../AppContext";
-import { getAvailableRegions } from "../config/RegionConfig";
-import "./RegionSelector.css";
-
-export function RegionSelector() {
- const { region, setRegion } = useApp();
- const regions = getAvailableRegions();
-
- const handleRegionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
- const newRegion = e.target.value as any;
- setRegion(newRegion);
- };
-
- return (
- <div className="region-selector">
- <label htmlFor="region-select" className="region-label">
- Región:
- </label>
- <select
- id="region-select"
- className="region-select"
- value={region}
- onChange={handleRegionChange}
- >
- {regions.map((r) => (
- <option key={r.id} value={r.id}>
- {r.name}
- </option>
- ))}
- </select>
- </div>
- );
-}
diff --git a/src/frontend/app/components/StopMapModal.tsx b/src/frontend/app/components/StopMapModal.tsx
index 1cb6d88..bb6a3fa 100644
--- a/src/frontend/app/components/StopMapModal.tsx
+++ b/src/frontend/app/components/StopMapModal.tsx
@@ -9,7 +9,7 @@ import React, {
import Map, { Layer, Marker, Source, type MapRef } from "react-map-gl/maplibre";
import { Sheet } from "react-modal-sheet";
import { useApp } from "~/AppContext";
-import { REGION_DATA } from "~/config/RegionConfig";
+import { APP_CONSTANTS } from "~/config/constants";
import { getLineColour } from "~/data/LineColors";
import type { Stop } from "~/data/StopDataProvider";
import { loadStyle } from "~/maps/styleloader";
@@ -243,7 +243,7 @@ export const StopMapModal: React.FC<StopMapModalProps> = ({
!selectedBus ||
!selectedBus.schedule?.shapeId ||
selectedBus.currentPosition?.shapeIndex === undefined ||
- !REGION_DATA.shapeEndpoint
+ !APP_CONSTANTS.shapeEndpoint
) {
setShapeData(null);
setPreviousShapeData(null);
@@ -263,7 +263,7 @@ export const StopMapModal: React.FC<StopMapModalProps> = ({
sLat?: number,
sLon?: number
) => {
- let url = `${REGION_DATA.shapeEndpoint}?shapeId=${sId}`;
+ let url = `${APP_CONSTANTS.shapeEndpoint}?shapeId=${sId}`;
if (bIndex !== undefined) url += `&busShapeIndex=${bIndex}`;
if (sIndex !== undefined) url += `&stopShapeIndex=${sIndex}`;
else if (sLat && sLon) url += `&stopLat=${sLat}&stopLon=${sLon}`;
diff --git a/src/frontend/app/components/StopSummarySheet.css b/src/frontend/app/components/map/StopSummarySheet.css
index 5869d41..5869d41 100644
--- a/src/frontend/app/components/StopSummarySheet.css
+++ b/src/frontend/app/components/map/StopSummarySheet.css
diff --git a/src/frontend/app/components/StopSummarySheet.tsx b/src/frontend/app/components/map/StopSummarySheet.tsx
index c2d6ffe..b24e71c 100644
--- a/src/frontend/app/components/StopSummarySheet.tsx
+++ b/src/frontend/app/components/map/StopSummarySheet.tsx
@@ -4,19 +4,27 @@ import { useTranslation } from "react-i18next";
import { Sheet } from "react-modal-sheet";
import { Link } from "react-router";
import { ConsolidatedCirculationList } from "~/components/Stops/ConsolidatedCirculationList";
-import { REGION_DATA } from "~/config/RegionConfig";
-import type { Stop } from "~/data/StopDataProvider";
-import { type ConsolidatedCirculation } from "../routes/stops-$id";
-import { ErrorDisplay } from "./ErrorDisplay";
-import LineIcon from "./LineIcon";
-import { StopAlert } from "./StopAlert";
+import { APP_CONSTANTS } from "~/config/constants";
+import { type ConsolidatedCirculation } from "../../routes/stops-$id";
+import { ErrorDisplay } from "../ErrorDisplay";
+import LineIcon from "../LineIcon";
import "./StopSummarySheet.css";
import { StopSummarySheetSkeleton } from "./StopSummarySheetSkeleton";
-interface StopSheetProps {
+export interface StopSheetProps {
isOpen: boolean;
onClose: () => void;
- stop: Stop;
+ stop: {
+ stopId: string;
+ stopCode?: string;
+ stopFeed?: string;
+ name: string;
+ lines: {
+ line: string;
+ colour?: string;
+ textColour?: string;
+ }[];
+ };
}
interface ErrorInfo {
@@ -29,7 +37,7 @@ const loadConsolidatedData = async (
stopId: string
): Promise<ConsolidatedCirculation[]> => {
const resp = await fetch(
- `${REGION_DATA.consolidatedCirculationsEndpoint}?stopId=${stopId}`,
+ `${APP_CONSTANTS.consolidatedCirculationsEndpoint}?stopId=${stopId}`,
{
headers: {
Accept: "application/json",
@@ -116,21 +124,24 @@ export const StopSheet: React.FC<StopSheetProps> = ({
<Sheet.Content drag="y">
<div className="stop-sheet-content">
<div className="stop-sheet-header">
- <h2 className="stop-sheet-title">{stop.name.original}</h2>
- <span className="stop-sheet-id">({stop.stopId})</span>
+ <h2 className="stop-sheet-title">{stop.name}</h2>
+ <span className="stop-sheet-id">({stop.stopCode})</span>
</div>
- <div
- className={`stop-sheet-lines-container ${stop.lines.length >= 10 ? "scrollable" : ""}`}
- >
- {stop.lines.map((line) => (
- <div key={line} className="stop-sheet-line-icon">
- <LineIcon line={line} mode="rounded" />
- </div>
+ <div className={`d-flex flex-wrap flex-row gap-2`}>
+ {stop.lines.map((lineObj) => (
+ <LineIcon
+ key={lineObj.line}
+ line={lineObj.line}
+ mode="pill"
+ colour={lineObj.colour}
+ textColour={lineObj.textColour}
+ />
))}
</div>
- <StopAlert stop={stop} compact />
+ {/* TODO: Enable stop alerts when available */}
+ {/*<StopAlert stop={stop} compact />*/}
{loading ? (
<StopSummarySheetSkeleton />
@@ -158,7 +169,7 @@ export const StopSheet: React.FC<StopSheetProps> = ({
) : (
<ConsolidatedCirculationList
data={data.slice(0, 4)}
- driver={stop.stopId.split(":")[0]}
+ driver={stop.stopFeed}
reduced
/>
)}
diff --git a/src/frontend/app/components/StopSummarySheetSkeleton.tsx b/src/frontend/app/components/map/StopSummarySheetSkeleton.tsx
index 7697efc..7697efc 100644
--- a/src/frontend/app/components/StopSummarySheetSkeleton.tsx
+++ b/src/frontend/app/components/map/StopSummarySheetSkeleton.tsx
diff --git a/src/frontend/app/config/RegionConfig.ts b/src/frontend/app/config/RegionConfig.ts
deleted file mode 100644
index d595b3f..0000000
--- a/src/frontend/app/config/RegionConfig.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import type { LngLatLike } from "maplibre-gl";
-
-export type RegionId = "vigo";
-
-export interface RegionData {
- id: RegionId;
- name: string;
- stopsEndpoint: string;
- estimatesEndpoint: string;
- consolidatedCirculationsEndpoint: string;
- timetableEndpoint: string;
- shapeEndpoint: string;
- defaultCenter: LngLatLike;
- bounds: {
- sw: LngLatLike;
- ne: LngLatLike;
- };
- textColour: string;
- defaultZoom: number;
- showMeters: boolean;
-}
-
-export const REGION_DATA: RegionData = {
- id: "vigo",
- name: "Vigo",
- stopsEndpoint: "/stops/vigo.json",
- estimatesEndpoint: "/api/vigo/GetStopEstimates",
- consolidatedCirculationsEndpoint: "/api/vigo/GetConsolidatedCirculations",
- timetableEndpoint: "/api/vigo/GetStopTimetable",
- shapeEndpoint: "/api/vigo/GetShape",
- defaultCenter: {
- lat: 42.229188855975046,
- lng: -8.72246955783102,
- } as LngLatLike,
- bounds: {
- sw: [-8.951059, 42.098923] as LngLatLike,
- ne: [-8.447748, 42.3496] as LngLatLike,
- },
- textColour: "#e72b37",
- defaultZoom: 14,
- showMeters: true,
-};
-
-export const getAvailableRegions = (): RegionData[] => [REGION_DATA];
diff --git a/src/frontend/app/config/constants.ts b/src/frontend/app/config/constants.ts
new file mode 100644
index 0000000..9a0fdd1
--- /dev/null
+++ b/src/frontend/app/config/constants.ts
@@ -0,0 +1,22 @@
+import type { LngLatLike } from "maplibre-gl";
+
+export type RegionId = "vigo";
+
+export const APP_CONSTANTS = {
+ id: "vigo",
+
+ stopsEndpoint: "/stops/vigo.json",
+ consolidatedCirculationsEndpoint: "/api/vigo/GetConsolidatedCirculations",
+ shapeEndpoint: "/api/vigo/GetShape",
+ defaultCenter: {
+ lat: 42.229188855975046,
+ lng: -8.72246955783102,
+ } as LngLatLike,
+ bounds: {
+ sw: [-9.629517, 41.463312] as LngLatLike,
+ ne: [-6.289673, 43.711564] as LngLatLike,
+ },
+ textColour: "#e72b37",
+ defaultZoom: 14,
+ showMeters: true,
+};
diff --git a/src/frontend/app/contexts/MapContext.tsx b/src/frontend/app/contexts/MapContext.tsx
index af13bb7..db1392c 100644
--- a/src/frontend/app/contexts/MapContext.tsx
+++ b/src/frontend/app/contexts/MapContext.tsx
@@ -6,7 +6,7 @@ import {
useState,
type ReactNode,
} from "react";
-import { REGION_DATA } from "~/config/RegionConfig";
+import { APP_CONSTANTS } from "~/config/constants";
interface MapState {
center: LngLatLike;
@@ -36,8 +36,8 @@ export const MapProvider = ({ children }: { children: ReactNode }) => {
// We might want to ensure we have a fallback if the region changed while the app was closed?
// But for now, let's stick to the existing logic.
return {
- center: parsed.center || REGION_DATA.defaultCenter,
- zoom: parsed.zoom || REGION_DATA.defaultZoom,
+ center: parsed.center || APP_CONSTANTS.defaultCenter,
+ zoom: parsed.zoom || APP_CONSTANTS.defaultZoom,
userLocation: parsed.userLocation || null,
hasLocationPermission: parsed.hasLocationPermission || false,
};
@@ -46,8 +46,8 @@ export const MapProvider = ({ children }: { children: ReactNode }) => {
}
}
return {
- center: REGION_DATA.defaultCenter,
- zoom: REGION_DATA.defaultZoom,
+ center: APP_CONSTANTS.defaultCenter,
+ zoom: APP_CONSTANTS.defaultZoom,
userLocation: null,
hasLocationPermission: false,
};
diff --git a/src/frontend/app/contexts/SettingsContext.tsx b/src/frontend/app/contexts/SettingsContext.tsx
index 5f6ff46..d66ee52 100644
--- a/src/frontend/app/contexts/SettingsContext.tsx
+++ b/src/frontend/app/contexts/SettingsContext.tsx
@@ -5,7 +5,7 @@ import {
useState,
type ReactNode,
} from "react";
-import { APP_CONFIG } from "../config/AppConfig";
+import { APP_CONFIG } from "~/config/AppConfig";
export type Theme = "light" | "dark" | "system";
export type TableStyle = "regular" | "grouped" | "experimental_consolidated";
diff --git a/src/frontend/app/data/SpecialPlacesProvider.ts b/src/frontend/app/data/SpecialPlacesProvider.ts
index 2e3be68..d11b119 100644
--- a/src/frontend/app/data/SpecialPlacesProvider.ts
+++ b/src/frontend/app/data/SpecialPlacesProvider.ts
@@ -1,5 +1,3 @@
-import { REGION_DATA } from "~/config/RegionConfig";
-
export interface SpecialPlace {
name: string;
type: "stop" | "address";
@@ -9,8 +7,8 @@ export interface SpecialPlace {
longitude?: number;
}
-const STORAGE_KEY_HOME = `specialPlace_home_${REGION_DATA.id}`;
-const STORAGE_KEY_WORK = `specialPlace_work_${REGION_DATA.id}`;
+const STORAGE_KEY_HOME = `specialPlace_home`;
+const STORAGE_KEY_WORK = `specialPlace_work`;
function getHome(): SpecialPlace | null {
try {
diff --git a/src/frontend/app/data/StopDataProvider.ts b/src/frontend/app/data/StopDataProvider.ts
index e523bd1..7bab10c 100644
--- a/src/frontend/app/data/StopDataProvider.ts
+++ b/src/frontend/app/data/StopDataProvider.ts
@@ -1,19 +1,13 @@
-import { REGION_DATA } from "~/config/RegionConfig";
+import { APP_CONSTANTS } from "~/config/constants";
export interface CachedStopList {
timestamp: number;
data: Stop[];
}
-export type StopName = {
- original: string;
- intersect?: string;
-};
-
export interface Stop {
stopId: string;
- type?: "bus" | "train";
- name: StopName;
+ name: string;
latitude?: number;
longitude?: number;
lines: string[];
@@ -41,13 +35,13 @@ function normalizeId(id: number | string): string {
// Initialize cachedStops and customNames once per region
async function initStops() {
- if (!cachedStopsByRegion[REGION_DATA.id]) {
- const response = await fetch(REGION_DATA.stopsEndpoint);
+ if (!cachedStopsByRegion[APP_CONSTANTS.id]) {
+ const response = await fetch(APP_CONSTANTS.stopsEndpoint);
const rawStops = (await response.json()) as any[];
// build array and map
- stopsMapByRegion[REGION_DATA.id] = {};
- cachedStopsByRegion[REGION_DATA.id] = rawStops.map((raw) => {
+ stopsMapByRegion[APP_CONSTANTS.id] = {};
+ cachedStopsByRegion[APP_CONSTANTS.id] = rawStops.map((raw) => {
const id = normalizeId(raw.stopId);
const entry = {
...raw,
@@ -55,21 +49,23 @@ async function initStops() {
type: raw.type || (id.startsWith("renfe:") ? "train" : "bus"),
favourite: false,
} as Stop;
- stopsMapByRegion[REGION_DATA.id][id] = entry;
+ stopsMapByRegion[APP_CONSTANTS.id][id] = entry;
return entry;
});
// load custom names
- const rawCustom = localStorage.getItem(`customStopNames_${REGION_DATA.id}`);
+ const rawCustom = localStorage.getItem(
+ `customStopNames_${APP_CONSTANTS.id}`
+ );
if (rawCustom) {
const parsed = JSON.parse(rawCustom);
const normalized: Record<string, string> = {};
for (const [key, value] of Object.entries(parsed)) {
normalized[normalizeId(key)] = value as string;
}
- customNamesByRegion[REGION_DATA.id] = normalized;
+ customNamesByRegion[APP_CONSTANTS.id] = normalized;
} else {
- customNamesByRegion[REGION_DATA.id] = {};
+ customNamesByRegion[APP_CONSTANTS.id] = {};
}
}
}
@@ -92,9 +88,9 @@ async function getStops(): Promise<Stop[]> {
async function getStopById(stopId: string | number): Promise<Stop | undefined> {
await initStops();
const id = normalizeId(stopId);
- const stop = stopsMapByRegion[REGION_DATA.id]?.[id];
+ const stop = stopsMapByRegion[APP_CONSTANTS.id]?.[id];
if (stop) {
- const rawFav = localStorage.getItem(`favouriteStops_${REGION_DATA.id}`);
+ const rawFav = localStorage.getItem(`favouriteStops_${APP_CONSTANTS.id}`);
const favouriteStops = rawFav
? (JSON.parse(rawFav) as (number | string)[]).map(normalizeId)
: [];
@@ -105,32 +101,29 @@ async function getStopById(stopId: string | number): Promise<Stop | undefined> {
// Updated display name to include custom names
function getDisplayName(stop: Stop): string {
- const customNames = customNamesByRegion[REGION_DATA.id] || {};
- if (customNames[stop.stopId]) return customNames[stop.stopId];
- const nameObj = stop.name;
- return nameObj.intersect || nameObj.original;
+ return stop.name;
}
// New: set or remove custom names
function setCustomName(stopId: string | number, label: string) {
const id = normalizeId(stopId);
- if (!customNamesByRegion[REGION_DATA.id]) {
- customNamesByRegion[REGION_DATA.id] = {};
+ if (!customNamesByRegion[APP_CONSTANTS.id]) {
+ customNamesByRegion[APP_CONSTANTS.id] = {};
}
- customNamesByRegion[REGION_DATA.id][id] = label;
+ customNamesByRegion[APP_CONSTANTS.id][id] = label;
localStorage.setItem(
- `customStopNames_${REGION_DATA.id}`,
- JSON.stringify(customNamesByRegion[REGION_DATA.id])
+ `customStopNames_${APP_CONSTANTS.id}`,
+ JSON.stringify(customNamesByRegion[APP_CONSTANTS.id])
);
}
function removeCustomName(stopId: string | number) {
const id = normalizeId(stopId);
- if (customNamesByRegion[REGION_DATA.id]?.[id]) {
- delete customNamesByRegion[REGION_DATA.id][id];
+ if (customNamesByRegion[APP_CONSTANTS.id]?.[id]) {
+ delete customNamesByRegion[APP_CONSTANTS.id][id];
localStorage.setItem(
- `customStopNames_${REGION_DATA.id}`,
- JSON.stringify(customNamesByRegion[REGION_DATA.id])
+ `customStopNames_${APP_CONSTANTS.id}`,
+ JSON.stringify(customNamesByRegion[APP_CONSTANTS.id])
);
}
}
@@ -138,7 +131,7 @@ function removeCustomName(stopId: string | number) {
// New: get custom label for a stop
function getCustomName(stopId: string | number): string | undefined {
const id = normalizeId(stopId);
- return customNamesByRegion[REGION_DATA.id]?.[id];
+ return customNamesByRegion[APP_CONSTANTS.id]?.[id];
}
function addFavourite(stopId: string | number) {
@@ -231,7 +224,7 @@ function getFavouriteIds(): string[] {
// New function to load stops from network
async function loadStopsFromNetwork(): Promise<Stop[]> {
- const response = await fetch(REGION_DATA.stopsEndpoint);
+ const response = await fetch(APP_CONSTANTS.stopsEndpoint);
const rawStops = (await response.json()) as any[];
return rawStops.map((raw) => {
const id = normalizeId(raw.stopId);
@@ -244,6 +237,10 @@ async function loadStopsFromNetwork(): Promise<Stop[]> {
});
}
+function getTileUrlTemplate(): string {
+ return window.location.origin + "/api/tiles/stops/{z}/{x}/{y}";
+}
+
export default {
getStops,
getStopById,
@@ -258,4 +255,5 @@ export default {
getRecent,
getFavouriteIds,
loadStopsFromNetwork,
+ getTileUrlTemplate,
};
diff --git a/src/frontend/app/routes/home.tsx b/src/frontend/app/routes/home.tsx
index e97659a..36565bd 100644
--- a/src/frontend/app/routes/home.tsx
+++ b/src/frontend/app/routes/home.tsx
@@ -31,7 +31,7 @@ export default function StopList() {
() =>
new Fuse(data || [], {
threshold: 0.3,
- keys: ["name.original", "name.intersect", "stopId"],
+ keys: ["name", "stopId"],
}),
[data]
);
diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx
index 39fc062..db9de59 100644
--- a/src/frontend/app/routes/map.tsx
+++ b/src/frontend/app/routes/map.tsx
@@ -1,8 +1,7 @@
-import StopDataProvider, { type Stop } from "../data/StopDataProvider";
+import StopDataProvider from "../data/StopDataProvider";
import "./map.css";
import { DEFAULT_STYLE, loadStyle } from "app/maps/styleloader";
-import type { Feature as GeoJsonFeature, Point } from "geojson";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Map, {
@@ -16,8 +15,11 @@ import Map, {
} from "react-map-gl/maplibre";
import { useNavigate } from "react-router";
import { PlannerOverlay } from "~/components/PlannerOverlay";
-import { StopSheet } from "~/components/StopSummarySheet";
-import { REGION_DATA } from "~/config/RegionConfig";
+import {
+ StopSheet,
+ type StopSheetProps,
+} from "~/components/map/StopSummarySheet";
+import { APP_CONSTANTS } from "~/config/constants";
import { usePageTitle } from "~/contexts/PageTitleContext";
import { usePlanner } from "~/hooks/usePlanner";
import { useApp } from "../AppContext";
@@ -28,19 +30,9 @@ export default function StopMap() {
const { t } = useTranslation();
const navigate = useNavigate();
usePageTitle(t("navbar.map", "Mapa"));
- const [stops, setStops] = useState<
- GeoJsonFeature<
- Point,
- {
- stopId: string;
- name: string;
- lines: string[];
- cancelled?: boolean;
- prefix: string;
- }
- >[]
- >([]);
- const [selectedStop, setSelectedStop] = useState<Stop | null>(null);
+ const [selectedStop, setSelectedStop] = useState<
+ StopSheetProps["stop"] | null
+ >(null);
const [isSheetOpen, setIsSheetOpen] = useState(false);
const { mapState, updateMapState, theme } = useApp();
const mapRef = useRef<MapRef>(null);
@@ -63,46 +55,11 @@ export default function StopMap() {
return;
}
const feature = features[0];
- console.debug("Map click feature:", feature);
- const props: any = feature.properties;
handlePointClick(feature);
};
useEffect(() => {
- StopDataProvider.getStops().then((data) => {
- const features: GeoJsonFeature<
- Point,
- {
- stopId: string;
- name: string;
- lines: string[];
- cancelled?: boolean;
- prefix: string;
- }
- >[] = data.map((s) => ({
- type: "Feature",
- geometry: {
- type: "Point",
- coordinates: [s.longitude as number, s.latitude as number],
- },
- properties: {
- stopId: s.stopId,
- name: s.name.original,
- lines: s.lines,
- cancelled: s.cancelled ?? false,
- prefix: s.stopId.startsWith("renfe:")
- ? "stop-renfe"
- : s.cancelled
- ? "stop-vitrasa-cancelled"
- : "stop-vitrasa",
- },
- }));
- setStops(features);
- });
- }, []);
-
- useEffect(() => {
//const styleName = "carto";
const styleName = "openfreemap";
loadStyle(styleName, theme)
@@ -166,26 +123,29 @@ export default function StopMap() {
const handlePointClick = (feature: any) => {
const props: any = feature.properties;
- if (!props || !props.stopId) {
+ // TODO: Move ID to constant, improve type checking
+ if (!props || feature.layer.id !== "stops") {
console.warn("Invalid feature properties:", props);
return;
}
- const stopId = props.stopId;
+ const stopId = props.id;
- // fetch full stop to get lines array
- StopDataProvider.getStopById(stopId)
- .then((stop) => {
- if (!stop) {
- console.warn("Stop not found:", stopId);
- return;
- }
- setSelectedStop(stop);
- setIsSheetOpen(true);
- })
- .catch((err) => {
- console.error("Error fetching stop details:", err);
- });
+ console.debug("Stop clicked:", stopId, props);
+
+ setSelectedStop({
+ stopId: props.id,
+ stopCode: props.code,
+ name: props.name || "Unknown Stop",
+ lines: JSON.parse(props.routes || "[]").map((route) => {
+ return {
+ line: route.shortName,
+ colour: route.colour,
+ textColour: route.textColour,
+ };
+ }),
+ });
+ setIsSheetOpen(true);
};
return (
@@ -203,7 +163,7 @@ export default function StopMap() {
style={{ width: "100%", height: "100%" }}
interactiveLayerIds={["stops", "stops-label"]}
onClick={onMapClick}
- minZoom={11}
+ minZoom={5}
scrollZoom
pitch={0}
roll={0}
@@ -214,7 +174,7 @@ export default function StopMap() {
zoom: mapState.zoom,
}}
attributionControl={{ compact: false }}
- maxBounds={[REGION_DATA.bounds.sw, REGION_DATA.bounds.ne]}
+ maxBounds={[APP_CONSTANTS.bounds.sw, APP_CONSTANTS.bounds.ne]}
>
<NavigationControl position="bottom-right" />
<GeolocateControl
@@ -225,8 +185,10 @@ export default function StopMap() {
<Source
id="stops-source"
- type="geojson"
- data={{ type: "FeatureCollection", features: stops }}
+ type="vector"
+ tiles={[StopDataProvider.getTileUrlTemplate()]}
+ minzoom={11}
+ maxzoom={20}
/>
<Layer
@@ -234,8 +196,26 @@ export default function StopMap() {
type="symbol"
minzoom={11}
source="stops-source"
+ source-layer="stops"
layout={{
- "icon-image": ["get", "prefix"],
+ // TODO: Fix ñapa by maybe including this from the server side?
+ "icon-image": [
+ "match",
+ ["get", "feed"],
+ "vitrasa",
+ "stop-vitrasa",
+ "santiago",
+ "stop-santiago",
+ "coruna",
+ "stop-coruna",
+ "xunta",
+ "stop-xunta",
+ "renfe",
+ "stop-renfe",
+ "feve",
+ "stop-feve",
+ "#stop-generic",
+ ],
"icon-size": [
"interpolate",
["linear"],
@@ -256,6 +236,7 @@ export default function StopMap() {
id="stops-label"
type="symbol"
source="stops-source"
+ source-layer="stops"
minzoom={16}
layout={{
"text-field": ["get", "name"],
@@ -267,10 +248,21 @@ export default function StopMap() {
}}
paint={{
"text-color": [
- "case",
- ["==", ["get", "prefix"], "stop-renfe"],
+ "match",
+ ["get", "feed"],
+ "vitrasa",
+ "#95D516",
+ "santiago",
+ "#508096",
+ "coruna",
+ "#E61C29",
+ "xunta",
+ "#007BC4",
+ "renfe",
"#870164",
- "#e72b37",
+ "feve",
+ "#EE3D32",
+ "#333333",
],
"text-halo-color": "#FFF",
"text-halo-width": 1,
diff --git a/src/frontend/app/routes/planner.tsx b/src/frontend/app/routes/planner.tsx
index b1a9813..9e44425 100644
--- a/src/frontend/app/routes/planner.tsx
+++ b/src/frontend/app/routes/planner.tsx
@@ -9,7 +9,7 @@ import { useLocation } from "react-router";
import { useApp } from "~/AppContext";
import LineIcon from "~/components/LineIcon";
import { PlannerOverlay } from "~/components/PlannerOverlay";
-import { REGION_DATA } from "~/config/RegionConfig";
+import { APP_CONSTANTS } from "~/config/constants";
import { usePageTitle } from "~/contexts/PageTitleContext";
import { type Itinerary } from "~/data/PlannerApi";
import { usePlanner } from "~/hooks/usePlanner";
@@ -392,7 +392,7 @@ const ItineraryDetail = ({
if (!arrivalsByStop[stopKey]) {
try {
const resp = await fetch(
- `${REGION_DATA.consolidatedCirculationsEndpoint}?stopId=${encodeURIComponent(leg.from.stopCode || leg.from.stopId)}`,
+ `${APP_CONSTANTS.consolidatedCirculationsEndpoint}?stopId=${encodeURIComponent(leg.from.stopCode || leg.from.stopId)}`,
{ headers: { Accept: "application/json" } }
);
@@ -424,9 +424,11 @@ const ItineraryDetail = ({
ref={mapRef}
initialViewState={{
longitude:
- origin?.lon || (REGION_DATA.defaultCenter as [number, number])[0],
+ origin?.lon ||
+ (APP_CONSTANTS.defaultCenter as [number, number])[0],
latitude:
- origin?.lat || (REGION_DATA.defaultCenter as [number, number])[1],
+ origin?.lat ||
+ (APP_CONSTANTS.defaultCenter as [number, number])[1],
zoom: 13,
}}
mapStyle={mapStyle}
diff --git a/src/frontend/app/routes/stops-$id.tsx b/src/frontend/app/routes/stops-$id.tsx
index 6d06215..46358dc 100644
--- a/src/frontend/app/routes/stops-$id.tsx
+++ b/src/frontend/app/routes/stops-$id.tsx
@@ -10,7 +10,7 @@ import { StopHelpModal } from "~/components/StopHelpModal";
import { StopMapModal } from "~/components/StopMapModal";
import { ConsolidatedCirculationList } from "~/components/Stops/ConsolidatedCirculationList";
import { ConsolidatedCirculationListSkeleton } from "~/components/Stops/ConsolidatedCirculationListSkeleton";
-import { REGION_DATA } from "~/config/RegionConfig";
+import { APP_CONSTANTS } from "~/config/constants";
import { usePageTitle } from "~/contexts/PageTitleContext";
import { useAutoRefresh } from "~/hooks/useAutoRefresh";
import StopDataProvider, { type Stop } from "../data/StopDataProvider";
@@ -58,7 +58,7 @@ const loadConsolidatedData = async (
stopId: string
): Promise<ConsolidatedCirculation[]> => {
const resp = await fetch(
- `${REGION_DATA.consolidatedCirculationsEndpoint}?stopId=${stopId}`,
+ `${APP_CONSTANTS.consolidatedCirculationsEndpoint}?stopId=${stopId}`,
{
headers: {
Accept: "application/json",
@@ -123,8 +123,7 @@ export default function Estimates() {
// Helper function to get the display name for the stop
const getStopDisplayName = useCallback(() => {
if (customName) return customName;
- if (stopData?.name.intersect) return stopData.name.intersect;
- if (stopData?.name.original) return stopData.name.original;
+ if (stopData?.name) return stopData.name;
return `Parada ${stopId}`;
}, [customName, stopData, stopId]);