aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/app')
-rw-r--r--src/frontend/app/components/PlannerOverlay.tsx9
-rw-r--r--src/frontend/app/components/StopGalleryItem.tsx11
-rw-r--r--src/frontend/app/components/StopItem.tsx11
-rw-r--r--src/frontend/app/components/stop/StopMapModal.tsx1
-rw-r--r--src/frontend/app/config/constants.ts1
-rw-r--r--src/frontend/app/data/StopDataProvider.ts258
-rw-r--r--src/frontend/app/routes/favourites.tsx28
-rw-r--r--src/frontend/app/routes/home.tsx185
-rw-r--r--src/frontend/app/routes/map.tsx44
9 files changed, 253 insertions, 295 deletions
diff --git a/src/frontend/app/components/PlannerOverlay.tsx b/src/frontend/app/components/PlannerOverlay.tsx
index 55e52d7..0320d45 100644
--- a/src/frontend/app/components/PlannerOverlay.tsx
+++ b/src/frontend/app/components/PlannerOverlay.tsx
@@ -110,10 +110,11 @@ export const PlannerOverlay: React.FC<PlannerOverlayProps> = ({
useEffect(() => {
// Load favourites once; used as local suggestions in the picker.
- StopDataProvider.getStops()
- .then((stops) =>
- stops
- .filter((s) => s.favourite && s.latitude && s.longitude)
+ const favouriteIds = StopDataProvider.getFavouriteIds();
+ StopDataProvider.fetchStopsByIds(favouriteIds)
+ .then((stopsMap) =>
+ Object.values(stopsMap)
+ .filter((s) => s.latitude && s.longitude)
.map(
(s) =>
({
diff --git a/src/frontend/app/components/StopGalleryItem.tsx b/src/frontend/app/components/StopGalleryItem.tsx
index bf60697..de369d8 100644
--- a/src/frontend/app/components/StopGalleryItem.tsx
+++ b/src/frontend/app/components/StopGalleryItem.tsx
@@ -26,7 +26,7 @@ const StopGalleryItem: React.FC<StopGalleryItemProps> = ({ stop }) => {
<span className="text-yellow-500 text-base">★</span>
)}
<span className="text-xs text-gray-600 dark:text-gray-400 font-medium">
- ({stop.stopId})
+ ({stop.stopCode || stop.stopId})
</span>
</div>
<div
@@ -41,8 +41,13 @@ const StopGalleryItem: React.FC<StopGalleryItemProps> = ({ stop }) => {
{StopDataProvider.getDisplayName(stop)}
</div>
<div className="flex flex-wrap gap-1 items-center">
- {stop.lines?.slice(0, 5).map((line) => (
- <LineIcon key={line} line={line} />
+ {stop.lines?.slice(0, 5).map((lineObj) => (
+ <LineIcon
+ key={lineObj.line}
+ line={lineObj.line}
+ colour={lineObj.colour}
+ textColour={lineObj.textColour}
+ />
))}
{stop.lines && stop.lines.length > 5 && (
<span className="text-xs text-gray-600 dark:text-gray-400 font-medium px-1.5 py-0.5 bg-gray-200 dark:bg-gray-700 rounded">
diff --git a/src/frontend/app/components/StopItem.tsx b/src/frontend/app/components/StopItem.tsx
index 9679b05..391e605 100644
--- a/src/frontend/app/components/StopItem.tsx
+++ b/src/frontend/app/components/StopItem.tsx
@@ -20,12 +20,17 @@ const StopItem: React.FC<StopItemProps> = ({ stop }) => {
{StopDataProvider.getDisplayName(stop)}
</span>
<span className="text-sm text-gray-600 dark:text-gray-400 ml-2">
- ({stop.stopId})
+ ({stop.stopCode || stop.stopId})
</span>
</div>
<div className="flex flex-wrap gap-1 mt-1">
- {stop.lines?.map((line) => (
- <LineIcon key={line} line={line} />
+ {stop.lines?.map((lineObj) => (
+ <LineIcon
+ key={lineObj.line}
+ line={lineObj.line}
+ colour={lineObj.colour}
+ textColour={lineObj.textColour}
+ />
))}
</div>
</Link>
diff --git a/src/frontend/app/components/stop/StopMapModal.tsx b/src/frontend/app/components/stop/StopMapModal.tsx
index 757411e..688ec2e 100644
--- a/src/frontend/app/components/stop/StopMapModal.tsx
+++ b/src/frontend/app/components/stop/StopMapModal.tsx
@@ -1,3 +1,4 @@
+import maplibregl from "maplibre-gl";
import React, {
useCallback,
useEffect,
diff --git a/src/frontend/app/config/constants.ts b/src/frontend/app/config/constants.ts
index 38ebb0b..a130f87 100644
--- a/src/frontend/app/config/constants.ts
+++ b/src/frontend/app/config/constants.ts
@@ -5,7 +5,6 @@ export type RegionId = "vigo";
export const APP_CONSTANTS = {
id: "vigo",
- stopsEndpoint: "/stops/vigo.json",
consolidatedCirculationsEndpoint: "/api/vigo/GetConsolidatedCirculations",
shapeEndpoint: "/api/vigo/GetShape",
defaultCenter: {
diff --git a/src/frontend/app/data/StopDataProvider.ts b/src/frontend/app/data/StopDataProvider.ts
index 697e171..76182c7 100644
--- a/src/frontend/app/data/StopDataProvider.ts
+++ b/src/frontend/app/data/StopDataProvider.ts
@@ -1,28 +1,31 @@
import { APP_CONSTANTS } from "~/config/constants";
-export interface CachedStopList {
- timestamp: number;
- data: Stop[];
-}
-
export interface Stop {
stopId: string;
+ stopCode?: string;
name: string;
latitude?: number;
longitude?: number;
- lines: string[];
+ lines: {
+ line: string;
+ colour: string;
+ textColour: string;
+ }[];
favourite?: boolean;
- amenities?: string[];
+ type?: "bus" | "coach" | "train" | "unknown";
+}
- title?: string;
- message?: string;
- alert?: "info" | "warning" | "error";
- cancelled?: boolean;
+interface CacheEntry {
+ stop: Stop;
+ timestamp: number;
}
-// In-memory cache and lookup map per region
-const cachedStopsByRegion: Record<string, Stop[] | null> = {};
-const stopsMapByRegion: Record<string, Record<string, Stop>> = {};
+const CACHE_KEY = `stops_cache_${APP_CONSTANTS.id}`;
+const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
+
+// In-memory cache for the current session
+const memoryCache: Record<string, Stop> = {};
+
// Custom names loaded from localStorage per region
const customNamesByRegion: Record<string, Record<string, string>> = {};
@@ -33,82 +36,105 @@ function normalizeId(id: number | string): string {
return `vitrasa:${s}`;
}
-// Initialize cachedStops and customNames once per region
-async function initStops() {
- if (!cachedStopsByRegion[APP_CONSTANTS.id]) {
- const response = await fetch(APP_CONSTANTS.stopsEndpoint);
- const rawStops = (await response.json()) as any[];
+function getPersistentCache(): Record<string, CacheEntry> {
+ const raw = localStorage.getItem(CACHE_KEY);
+ if (!raw) return {};
+ try {
+ return JSON.parse(raw);
+ } catch {
+ return {};
+ }
+}
- // build array and map
- stopsMapByRegion[APP_CONSTANTS.id] = {};
- cachedStopsByRegion[APP_CONSTANTS.id] = rawStops.map((raw) => {
- const id = normalizeId(raw.stopId);
- const entry = {
- ...raw,
- stopId: id,
- type: raw.type || (id.startsWith("renfe:") ? "train" : "bus"),
- favourite: false,
- } as Stop;
- stopsMapByRegion[APP_CONSTANTS.id][id] = entry;
- return entry;
- });
+function savePersistentCache(cache: Record<string, CacheEntry>) {
+ localStorage.setItem(CACHE_KEY, JSON.stringify(cache));
+}
- // load custom names
- 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[APP_CONSTANTS.id] = normalized;
- } else {
- customNamesByRegion[APP_CONSTANTS.id] = {};
+async function fetchStopsByIds(ids: string[]): Promise<Record<string, Stop>> {
+ if (ids.length === 0) return {};
+
+ const normalizedIds = ids.map(normalizeId);
+ const now = Date.now();
+ const persistentCache = getPersistentCache();
+ const result: Record<string, Stop> = {};
+ const toFetch: string[] = [];
+
+ for (const id of normalizedIds) {
+ if (memoryCache[id]) {
+ result[id] = memoryCache[id];
+ continue;
+ }
+
+ const cached = persistentCache[id];
+ if (cached && now - cached.timestamp < CACHE_DURATION) {
+ memoryCache[id] = cached.stop;
+ result[id] = cached.stop;
+ continue;
}
+
+ toFetch.push(id);
}
-}
-async function getStops(): Promise<Stop[]> {
- await initStops();
- // update favourites
- const rawFav = localStorage.getItem("favouriteStops");
- const favouriteStops = rawFav
- ? (JSON.parse(rawFav) as (number | string)[]).map(normalizeId)
- : [];
+ if (toFetch.length > 0) {
+ try {
+ const response = await fetch(`/api/stops?ids=${toFetch.join(",")}`);
+ if (!response.ok) throw new Error("Failed to fetch stops");
- cachedStopsByRegion["vigo"]!.forEach(
- (stop) => (stop.favourite = favouriteStops.includes(stop.stopId))
- );
- return cachedStopsByRegion["vigo"]!;
+ const data = await response.json();
+ for (const [id, stopData] of Object.entries(data)) {
+ const stop: Stop = {
+ stopId: (stopData as any).id,
+ stopCode: (stopData as any).code,
+ name: (stopData as any).name,
+ lines: (stopData as any).routes.map((r: any) => ({
+ line: r.shortName,
+ colour: r.colour,
+ textColour: r.textColour,
+ })),
+ type: (stopData as any).id.startsWith("renfe:")
+ ? "train"
+ : (stopData as any).id.startsWith("xunta:")
+ ? "coach"
+ : "bus",
+ };
+
+ memoryCache[id] = stop;
+ result[id] = stop;
+ persistentCache[id] = { stop, timestamp: now };
+ }
+ savePersistentCache(persistentCache);
+ } catch (error) {
+ console.error("Error fetching stops:", error);
+ }
+ }
+
+ return result;
}
-// New: get single stop by id
async function getStopById(stopId: string | number): Promise<Stop | undefined> {
- await initStops();
const id = normalizeId(stopId);
- const stop = stopsMapByRegion[APP_CONSTANTS.id]?.[id];
+ const stops = await fetchStopsByIds([id]);
+ const stop = stops[id];
if (stop) {
- const rawFav = localStorage.getItem(`favouriteStops_${APP_CONSTANTS.id}`);
- const favouriteStops = rawFav
- ? (JSON.parse(rawFav) as (number | string)[]).map(normalizeId)
- : [];
- stop.favourite = favouriteStops.includes(id);
+ stop.favourite = isFavourite(id);
}
return stop;
}
-// Updated display name to include custom names
function getDisplayName(stop: Stop): string {
- return stop.name;
+ const custom = getCustomName(stop.stopId);
+ return custom || stop.name;
}
-// New: set or remove custom names
function setCustomName(stopId: string | number, label: string) {
const id = normalizeId(stopId);
if (!customNamesByRegion[APP_CONSTANTS.id]) {
- customNamesByRegion[APP_CONSTANTS.id] = {};
+ const rawCustom = localStorage.getItem(
+ `customStopNames_${APP_CONSTANTS.id}`
+ );
+ customNamesByRegion[APP_CONSTANTS.id] = rawCustom
+ ? JSON.parse(rawCustom)
+ : {};
}
customNamesByRegion[APP_CONSTANTS.id][id] = label;
localStorage.setItem(
@@ -119,7 +145,15 @@ function setCustomName(stopId: string | number, label: string) {
function removeCustomName(stopId: string | number) {
const id = normalizeId(stopId);
- if (customNamesByRegion[APP_CONSTANTS.id]?.[id]) {
+ if (!customNamesByRegion[APP_CONSTANTS.id]) {
+ const rawCustom = localStorage.getItem(
+ `customStopNames_${APP_CONSTANTS.id}`
+ );
+ customNamesByRegion[APP_CONSTANTS.id] = rawCustom
+ ? JSON.parse(rawCustom)
+ : {};
+ }
+ if (customNamesByRegion[APP_CONSTANTS.id][id]) {
delete customNamesByRegion[APP_CONSTANTS.id][id];
localStorage.setItem(
`customStopNames_${APP_CONSTANTS.id}`,
@@ -128,15 +162,24 @@ 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[APP_CONSTANTS.id]?.[id];
+ if (!customNamesByRegion[APP_CONSTANTS.id]) {
+ const rawCustom = localStorage.getItem(
+ `customStopNames_${APP_CONSTANTS.id}`
+ );
+ customNamesByRegion[APP_CONSTANTS.id] = rawCustom
+ ? JSON.parse(rawCustom)
+ : {};
+ }
+ return customNamesByRegion[APP_CONSTANTS.id][id];
}
function addFavourite(stopId: string | number) {
const id = normalizeId(stopId);
- const rawFavouriteStops = localStorage.getItem(`favouriteStops`);
+ const rawFavouriteStops = localStorage.getItem(
+ `favouriteStops_${APP_CONSTANTS.id}`
+ );
let favouriteStops: string[] = [];
if (rawFavouriteStops) {
favouriteStops = (JSON.parse(rawFavouriteStops) as (number | string)[]).map(
@@ -146,13 +189,18 @@ function addFavourite(stopId: string | number) {
if (!favouriteStops.includes(id)) {
favouriteStops.push(id);
- localStorage.setItem(`favouriteStops`, JSON.stringify(favouriteStops));
+ localStorage.setItem(
+ `favouriteStops_${APP_CONSTANTS.id}`,
+ JSON.stringify(favouriteStops)
+ );
}
}
function removeFavourite(stopId: string | number) {
const id = normalizeId(stopId);
- const rawFavouriteStops = localStorage.getItem(`favouriteStops`);
+ const rawFavouriteStops = localStorage.getItem(
+ `favouriteStops_${APP_CONSTANTS.id}`
+ );
let favouriteStops: string[] = [];
if (rawFavouriteStops) {
favouriteStops = (JSON.parse(rawFavouriteStops) as (number | string)[]).map(
@@ -161,12 +209,17 @@ function removeFavourite(stopId: string | number) {
}
const newFavouriteStops = favouriteStops.filter((sid) => sid !== id);
- localStorage.setItem(`favouriteStops`, JSON.stringify(newFavouriteStops));
+ localStorage.setItem(
+ `favouriteStops_${APP_CONSTANTS.id}`,
+ JSON.stringify(newFavouriteStops)
+ );
}
function isFavourite(stopId: string | number): boolean {
const id = normalizeId(stopId);
- const rawFavouriteStops = localStorage.getItem(`favouriteStops`);
+ const rawFavouriteStops = localStorage.getItem(
+ `favouriteStops_${APP_CONSTANTS.id}`
+ );
if (rawFavouriteStops) {
const favouriteStops = (
JSON.parse(rawFavouriteStops) as (number | string)[]
@@ -180,29 +233,34 @@ const RECENT_STOPS_LIMIT = 10;
function pushRecent(stopId: string | number) {
const id = normalizeId(stopId);
- const rawRecentStops = localStorage.getItem(`recentStops_vigo`);
- let recentStops: Set<string> = new Set();
+ const rawRecentStops = localStorage.getItem(
+ `recentStops_${APP_CONSTANTS.id}`
+ );
+ let recentStops: string[] = [];
if (rawRecentStops) {
- recentStops = new Set(
- (JSON.parse(rawRecentStops) as (number | string)[]).map(normalizeId)
+ recentStops = (JSON.parse(rawRecentStops) as (number | string)[]).map(
+ normalizeId
);
}
- recentStops.add(id);
- if (recentStops.size > RECENT_STOPS_LIMIT) {
- const iterator = recentStops.values();
- const val = iterator.next().value as string;
- recentStops.delete(val);
+ // Remove if already exists to move to front
+ recentStops = recentStops.filter((sid) => sid !== id);
+ recentStops.unshift(id);
+
+ if (recentStops.length > RECENT_STOPS_LIMIT) {
+ recentStops = recentStops.slice(0, RECENT_STOPS_LIMIT);
}
localStorage.setItem(
- `recentStops_vigo`,
- JSON.stringify(Array.from(recentStops))
+ `recentStops_${APP_CONSTANTS.id}`,
+ JSON.stringify(recentStops)
);
}
function getRecent(): string[] {
- const rawRecentStops = localStorage.getItem(`recentStops_vigo`);
+ const rawRecentStops = localStorage.getItem(
+ `recentStops_${APP_CONSTANTS.id}`
+ );
if (rawRecentStops) {
return (JSON.parse(rawRecentStops) as (number | string)[]).map(normalizeId);
}
@@ -210,7 +268,9 @@ function getRecent(): string[] {
}
function getFavouriteIds(): string[] {
- const rawFavouriteStops = localStorage.getItem(`favouriteStops`);
+ const rawFavouriteStops = localStorage.getItem(
+ `favouriteStops_${APP_CONSTANTS.id}`
+ );
if (rawFavouriteStops) {
return (JSON.parse(rawFavouriteStops) as (number | string)[]).map(
normalizeId
@@ -219,28 +279,13 @@ function getFavouriteIds(): string[] {
return [];
}
-// New function to load stops from network
-async function loadStopsFromNetwork(): Promise<Stop[]> {
- const response = await fetch(APP_CONSTANTS.stopsEndpoint);
- const rawStops = (await response.json()) as any[];
- return rawStops.map((raw) => {
- const id = normalizeId(raw.stopId);
- return {
- ...raw,
- stopId: id,
- type: raw.type || (id.startsWith("renfe:") ? "train" : "bus"),
- favourite: false,
- } as Stop;
- });
-}
-
function getTileUrlTemplate(): string {
return window.location.origin + "/api/tiles/stops/{z}/{x}/{y}";
}
export default {
- getStops,
getStopById,
+ fetchStopsByIds,
getCustomName,
getDisplayName,
setCustomName,
@@ -251,6 +296,5 @@ export default {
pushRecent,
getRecent,
getFavouriteIds,
- loadStopsFromNetwork,
getTileUrlTemplate,
};
diff --git a/src/frontend/app/routes/favourites.tsx b/src/frontend/app/routes/favourites.tsx
index deb3629..c05ab11 100644
--- a/src/frontend/app/routes/favourites.tsx
+++ b/src/frontend/app/routes/favourites.tsx
@@ -27,10 +27,11 @@ export default function Favourites() {
// Load favourite stops
const favouriteIds = StopDataProvider.getFavouriteIds();
- const allStops = await StopDataProvider.getStops();
- const favStops = allStops.filter((stop) =>
- favouriteIds.includes(stop.stopId)
- );
+ const stopsMap = await StopDataProvider.fetchStopsByIds(favouriteIds);
+ const favStops = favouriteIds
+ .map((id) => stopsMap[id])
+ .filter(Boolean)
+ .map((stop) => ({ ...stop, favourite: true }));
setFavouriteStops(favStops);
} catch (error) {
console.error("Error loading favourites:", error);
@@ -190,14 +191,10 @@ function SpecialPlaceCard({
{icon}
</span>
<div className="flex-1 min-w-0">
- <h3 className="font-semibold text-text mb-1">
- {label}
- </h3>
+ <h3 className="font-semibold text-text mb-1">{label}</h3>
{place ? (
<div className="text-sm text-muted">
- <p className="font-medium text-text">
- {place.name}
- </p>
+ <p className="font-medium text-text">{place.name}</p>
{place.type === "stop" && place.stopId && (
<p className="text-xs mt-1">({place.stopId})</p>
)}
@@ -283,15 +280,20 @@ function FavouriteStopItem({
</span>
<span className="text-xs text-muted font-medium">
- ({stop.stopId})
+ ({stop.stopCode || stop.stopId})
</span>
</div>
<div className="font-semibold text-text mb-2">
{StopDataProvider.getDisplayName(stop)}
</div>
<div className="flex flex-wrap gap-1 items-center">
- {stop.lines?.slice(0, 6).map((line) => (
- <LineIcon key={line} line={line} />
+ {stop.lines?.slice(0, 6).map((lineObj) => (
+ <LineIcon
+ key={lineObj.line}
+ line={lineObj.line}
+ colour={lineObj.colour}
+ textColour={lineObj.textColour}
+ />
))}
{stop.lines && stop.lines.length > 6 && (
<span className="text-xs text-gray-500 dark:text-gray-400 ml-1">
diff --git a/src/frontend/app/routes/home.tsx b/src/frontend/app/routes/home.tsx
index b20a349..3e7f12d 100644
--- a/src/frontend/app/routes/home.tsx
+++ b/src/frontend/app/routes/home.tsx
@@ -1,4 +1,3 @@
-import Fuse from "fuse.js";
import { History } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -8,7 +7,6 @@ import { usePageTitle } from "~/contexts/PageTitleContext";
import { usePlanner } from "~/hooks/usePlanner";
import StopGallery from "../components/StopGallery";
import StopItem from "../components/StopItem";
-import StopItemSkeleton from "../components/StopItemSkeleton";
import StopDataProvider, { type Stop } from "../data/StopDataProvider";
import "../tailwind-full.css";
@@ -33,15 +31,6 @@ export default function StopList() {
[t]
);
- const fuse = useMemo(
- () =>
- new Fuse(data || [], {
- threshold: 0.3,
- keys: ["name", "stopId"],
- }),
- [data]
- );
-
const requestUserLocation = useCallback(() => {
if (typeof window === "undefined" || !("geolocation" in navigator)) {
return;
@@ -103,90 +92,33 @@ export default function StopList() {
};
}, [requestUserLocation]);
- // Sort stops by proximity when we know where the user is located.
- const sortedAllStops = useMemo(() => {
- if (!data) {
- return [] as Stop[];
- }
-
- if (!userLocation) {
- return [...data].sort((a, b) => a.stopId.localeCompare(b.stopId));
- }
-
- const toRadians = (value: number) => (value * Math.PI) / 180;
- const getDistance = (
- lat1: number,
- lon1: number,
- lat2: number,
- lon2: number
- ) => {
- const R = 6371000; // meters
- const dLat = toRadians(lat2 - lat1);
- const dLon = toRadians(lon2 - lon1);
- const a =
- Math.sin(dLat / 2) * Math.sin(dLat / 2) +
- Math.cos(toRadians(lat1)) *
- Math.cos(toRadians(lat2)) *
- Math.sin(dLon / 2) *
- Math.sin(dLon / 2);
- const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
- return R * c;
- };
-
- return data
- .map((stop) => {
- if (
- typeof stop.latitude !== "number" ||
- typeof stop.longitude !== "number"
- ) {
- return { stop, distance: Number.POSITIVE_INFINITY };
- }
-
- const distance = getDistance(
- userLocation.latitude,
- userLocation.longitude,
- stop.latitude,
- stop.longitude
- );
-
- return { stop, distance };
- })
- .sort((a, b) => {
- if (a.distance === b.distance) {
- return a.stop.stopId.localeCompare(b.stop.stopId);
- }
- return a.distance - b.distance;
- })
- .map(({ stop }) => stop);
- }, [data, userLocation]);
-
// Load stops from network
const loadStops = useCallback(async () => {
try {
setLoading(true);
- const stops = await StopDataProvider.loadStopsFromNetwork();
-
- // Add favourite flags to stops
- const favouriteStopsIds = StopDataProvider.getFavouriteIds();
- const stopsWithFavourites = stops.map((stop) => ({
- ...stop,
- favourite: favouriteStopsIds.includes(stop.stopId),
- }));
+ const favouriteIds = StopDataProvider.getFavouriteIds();
+ const recentIds = StopDataProvider.getRecent();
+ const allIds = Array.from(new Set([...favouriteIds, ...recentIds]));
- setData(stopsWithFavourites);
+ const stopsMap = await StopDataProvider.fetchStopsByIds(allIds);
- // Update favourite and recent stops with full data
- const favStops = stopsWithFavourites.filter((stop) =>
- favouriteStopsIds.includes(stop.stopId)
- );
+ const favStops = favouriteIds
+ .map((id) => stopsMap[id])
+ .filter(Boolean)
+ .map((stop) => ({ ...stop, favourite: true }));
setFavouriteStops(favStops);
- const recIds = StopDataProvider.getRecent();
- const recStops = recIds
- .map((id) => stopsWithFavourites.find((stop) => stop.stopId === id))
- .filter(Boolean) as Stop[];
- setRecentStops(recStops.reverse());
+ const recStops = recentIds
+ .map((id) => stopsMap[id])
+ .filter(Boolean)
+ .map((stop) => ({
+ ...stop,
+ favourite: favouriteIds.includes(stop.stopId),
+ }));
+ setRecentStops(recStops);
+
+ setData(Object.values(stopsMap));
} catch (error) {
console.error("Failed to load stops:", error);
} finally {
@@ -205,41 +137,14 @@ export default function StopList() {
clearTimeout(searchTimeout.current);
}
- searchTimeout.current = setTimeout(() => {
+ searchTimeout.current = setTimeout(async () => {
if (searchQuery.length === 0) {
setSearchResults(null);
return;
}
- if (!data) {
- console.error("No data available for search");
- return;
- }
-
- // Check if search query is a number (stop code search)
- const isNumericSearch = /^\d+$/.test(searchQuery.trim());
-
- let items: Stop[];
- if (isNumericSearch) {
- // Direct match for stop codes
- const stopId = searchQuery.trim();
- const exactMatch = data.filter(
- (stop) => stop.stopId === stopId || stop.stopId.endsWith(`:${stopId}`)
- );
- if (exactMatch.length > 0) {
- items = exactMatch;
- } else {
- // Fuzzy search if no exact match
- const results = fuse.search(searchQuery);
- items = results.map((result) => result.item);
- }
- } else {
- // Text search using Fuse.js
- const results = fuse.search(searchQuery);
- items = results.map((result) => result.item);
- }
-
- setSearchResults(items);
+ // Placeholder for future backend search
+ setSearchResults([]);
}, 300);
};
@@ -370,54 +275,6 @@ export default function StopList() {
)}
{/*<ServiceAlerts />*/}
-
- {/* All Stops / Nearby Stops */}
- <div className="w-full px-4 flex flex-col gap-2">
- <div className="flex items-center gap-2">
- {userLocation && (
- <svg
- className="w-5 h-5 text-blue-600 dark:text-blue-400"
- fill="none"
- viewBox="0 0 24 24"
- stroke="currentColor"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
- />
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
- />
- </svg>
- )}
- <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
- {userLocation
- ? t("stoplist.nearby_stops", "Nearby stops")
- : t("stoplist.all_stops", "Paradas")}
- </h2>
- </div>
-
- <ul className="list-none p-0 m-0 flex flex-col gap-2 md:grid md:grid-cols-[repeat(auto-fill,minmax(300px,1fr))] lg:grid-cols-[repeat(auto-fill,minmax(320px,1fr))]">
- {loading && (
- <>
- {Array.from({ length: 6 }, (_, index) => (
- <StopItemSkeleton key={`skeleton-${index}`} />
- ))}
- </>
- )}
- {!loading && data
- ? (userLocation
- ? sortedAllStops.slice(0, 6)
- : sortedAllStops
- ).map((stop) => <StopItem key={stop.stopId} stop={stop} />)
- : null}
- </ul>
- </div>
</>
)}
</div>
diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx
index b02c494..b8f179c 100644
--- a/src/frontend/app/routes/map.tsx
+++ b/src/frontend/app/routes/map.tsx
@@ -40,6 +40,13 @@ export default function StopMap() {
const { searchRoute } = usePlanner({ autoLoad: false });
+ const favouriteIds = useMemo(() => StopDataProvider.getFavouriteIds(), []);
+
+ const favouriteFilter = useMemo(() => {
+ if (favouriteIds.length === 0) return ["boolean", false];
+ return ["match", ["get", "id"], favouriteIds, true, false];
+ }, [favouriteIds]);
+
// Handle click events on clusters and individual stops
const onMapClick = (e: MapLayerMouseEvent) => {
const features = e.features;
@@ -140,6 +147,32 @@ export default function StopMap() {
/>
<Layer
+ id="stops-favourite-highlight"
+ type="circle"
+ minzoom={11}
+ source="stops-source"
+ source-layer="stops"
+ filter={["all", stopLayerFilter, favouriteFilter]}
+ paint={{
+ "circle-color": "#FFD700",
+ "circle-radius": [
+ "interpolate",
+ ["linear"],
+ ["zoom"],
+ 13,
+ 10,
+ 16,
+ 12,
+ 18,
+ 16,
+ ],
+ "circle-opacity": 0.4,
+ "circle-stroke-color": "#FFD700",
+ "circle-stroke-width": 2,
+ }}
+ />
+
+ <Layer
id="stops"
type="symbol"
minzoom={11}
@@ -161,6 +194,17 @@ export default function StopMap() {
],
"icon-allow-overlap": true,
"icon-ignore-placement": true,
+ "symbol-sort-key": [
+ "match",
+ ["get", "transitKind"],
+ "bus",
+ 3,
+ "coach",
+ 2,
+ "train",
+ 1,
+ 0,
+ ],
}}
/>