aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-04-20 20:15:55 +0200
committerAriel Costas Guerrero <ariel@costas.dev>2025-04-20 20:15:55 +0200
commit3676b1d1d9216a676c7d5a40affa5b3256ca8df3 (patch)
treeefa63a0d21ae52e32e405fe7b4ce56b02d782e86 /src
parentc86b4655f72c86362c064dd50bb701782b39e6eb (diff)
Refactor stop data handling with caching and custom names support
Diffstat (limited to 'src')
-rw-r--r--src/data/StopDataProvider.ts101
-rw-r--r--src/pages/Estimates.tsx28
-rw-r--r--src/pages/Map.tsx14
-rw-r--r--src/pages/StopList.tsx16
-rw-r--r--src/styles/Estimates.css80
5 files changed, 156 insertions, 83 deletions
diff --git a/src/data/StopDataProvider.ts b/src/data/StopDataProvider.ts
index 55d0e78..0c1e46e 100644
--- a/src/data/StopDataProvider.ts
+++ b/src/data/StopDataProvider.ts
@@ -17,41 +17,72 @@ export interface Stop {
favourite?: boolean;
}
-export default {
- getStops,
- getDisplayName,
- addFavourite,
- removeFavourite,
- isFavourite,
- pushRecent,
- getRecent
-};
+// In-memory cache and lookup map
+let cachedStops: Stop[] | null = null;
+let stopsMap: Record<number, Stop> = {};
+// Custom names loaded from localStorage
+let customNames: Record<number, string> = {};
-async function getStops(): Promise<Stop[]> {
- const rawFavouriteStops = localStorage.getItem('favouriteStops');
- let favouriteStops: number[] = [];
- if (rawFavouriteStops) {
- favouriteStops = JSON.parse(rawFavouriteStops) as number[];
- }
+// Initialize cachedStops and customNames once
+async function initStops() {
+ if (!cachedStops) {
+ const response = await fetch('/stops.json');
+ const stops = await response.json() as Stop[];
+ // build array and map
+ stopsMap = {};
+ cachedStops = stops.map(stop => {
+ const entry = { ...stop, favourite: false } as Stop;
+ stopsMap[stop.stopId] = entry;
+ return entry;
+ });
+ // load custom names
+ const rawCustom = localStorage.getItem('customStopNames');
+ if (rawCustom) customNames = JSON.parse(rawCustom) as Record<number, string>;
+ }
+}
- const response = await fetch('/stops.json');
- const stops = await response.json() as Stop[];
+async function getStops(): Promise<Stop[]> {
+ await initStops();
+ // update favourites
+ const rawFav = localStorage.getItem('favouriteStops');
+ const favouriteStops = rawFav ? JSON.parse(rawFav) as number[] : [];
+ cachedStops!.forEach(stop => stop.favourite = favouriteStops.includes(stop.stopId));
+ return cachedStops!;
+}
- return stops.map((stop: Stop) => {
- return {
- ...stop,
- favourite: favouriteStops.includes(stop.stopId)
- };
- });
+// New: get single stop by id
+async function getStopById(stopId: number): Promise<Stop | undefined> {
+ await initStops();
+ const stop = stopsMap[stopId];
+ if (stop) {
+ const rawFav = localStorage.getItem('favouriteStops');
+ const favouriteStops = rawFav ? JSON.parse(rawFav) as number[] : [];
+ stop.favourite = favouriteStops.includes(stopId);
+ }
+ return stop;
}
-// Get display name based on preferences or context
+// Updated display name to include custom names
function getDisplayName(stop: Stop): string {
- if (typeof stop.name === 'string') {
- return stop.name;
- }
+ if (customNames[stop.stopId]) return customNames[stop.stopId];
+ const nameObj = stop.name;
+ return nameObj.intersect || nameObj.original;
+}
+
+// New: set or remove custom names
+function setCustomName(stopId: number, label: string) {
+ customNames[stopId] = label;
+ localStorage.setItem('customStopNames', JSON.stringify(customNames));
+}
- return stop.name.intersect || stop.name.original;
+function removeCustomName(stopId: number) {
+ delete customNames[stopId];
+ localStorage.setItem('customStopNames', JSON.stringify(customNames));
+}
+
+// New: get custom label for a stop
+function getCustomName(stopId: number): string | undefined {
+ return customNames[stopId];
}
function addFavourite(stopId: number) {
@@ -113,3 +144,17 @@ function getRecent(): number[] {
}
return [];
}
+
+export default {
+ getStops,
+ getStopById,
+ getCustomName,
+ getDisplayName,
+ setCustomName,
+ removeCustomName,
+ addFavourite,
+ removeFavourite,
+ isFavourite,
+ pushRecent,
+ getRecent
+};
diff --git a/src/pages/Estimates.tsx b/src/pages/Estimates.tsx
index 90745da..7cf941a 100644
--- a/src/pages/Estimates.tsx
+++ b/src/pages/Estimates.tsx
@@ -1,7 +1,7 @@
import { JSX, useEffect, useState } from "react";
import { useParams } from "react-router";
import StopDataProvider from "../data/StopDataProvider";
-import { Star } from 'lucide-react';
+import { Star, Edit2 } from 'lucide-react';
import "../styles/Estimates.css";
import { RegularTable } from "../components/RegularTable";
import { useApp } from "../AppContext";
@@ -28,10 +28,12 @@ const loadData = async (stopId: string) => {
};
export function Estimates(): JSX.Element {
+ const params = useParams();
+ const stopIdNum = parseInt(params.stopId ?? "");
+ const [customName, setCustomName] = useState<string | undefined>(undefined);
const [data, setData] = useState<StopDetails | null>(null);
const [dataDate, setDataDate] = useState<Date | null>(null);
const [favourited, setFavourited] = useState(false);
- const params = useParams();
const { tableStyle } = useApp();
useEffect(() => {
@@ -39,6 +41,7 @@ export function Estimates(): JSX.Element {
.then((body: StopDetails) => {
setData(body);
setDataDate(new Date());
+ setCustomName(StopDataProvider.getCustomName(stopIdNum));
})
@@ -52,14 +55,28 @@ export function Estimates(): JSX.Element {
const toggleFavourite = () => {
if (favourited) {
- StopDataProvider.removeFavourite(parseInt(params.stopId ?? ""));
+ StopDataProvider.removeFavourite(stopIdNum);
setFavourited(false);
} else {
- StopDataProvider.addFavourite(parseInt(params.stopId ?? ""));
+ StopDataProvider.addFavourite(stopIdNum);
setFavourited(true);
}
}
+ const handleRename = () => {
+ const current = customName ?? data?.stop.name;
+ const input = window.prompt('Custom name for this stop:', current);
+ if (input === null) return; // cancelled
+ const trimmed = input.trim();
+ if (trimmed === '') {
+ StopDataProvider.removeCustomName(stopIdNum);
+ setCustomName(undefined);
+ } else {
+ StopDataProvider.setCustomName(stopIdNum, trimmed);
+ setCustomName(trimmed);
+ }
+ };
+
if (data === null) return <h1 className="page-title">Cargando datos en tiempo real...</h1>
return (
@@ -67,7 +84,8 @@ export function Estimates(): JSX.Element {
<div className="estimates-header">
<h1 className="page-title">
<Star className={`star-icon ${favourited ? 'active' : ''}`} onClick={toggleFavourite} />
- {data?.stop.name} <span className="estimates-stop-id">({data?.stop.id})</span>
+ <Edit2 className="edit-icon" onClick={handleRename} />
+ {(customName ?? data.stop.name)} <span className="estimates-stop-id">({data.stop.id})</span>
</h1>
</div>
diff --git a/src/pages/Map.tsx b/src/pages/Map.tsx
index af95bf9..1f0a9e0 100644
--- a/src/pages/Map.tsx
+++ b/src/pages/Map.tsx
@@ -41,17 +41,9 @@ export function StopMap() {
const { mapState } = useApp();
useEffect(() => {
- StopDataProvider.getStops().then((stops) => { setStops(stops); });
+ StopDataProvider.getStops().then(setStops);
}, []);
- const getDisplayName = (stop: Stop): string => {
- if (typeof stop.name === 'string') {
- return stop.name;
- }
-
- return stop.name.intersect || stop.name.original;
- }
-
return (
<MapContainer
center={mapState.center}
@@ -66,10 +58,10 @@ export function StopMap() {
<EnhancedLocateControl />
<MapEventHandler />
<MarkerClusterGroup>
- {stops.map((stop) => (
+ {stops.map(stop => (
<Marker key={stop.stopId} position={[stop.latitude, stop.longitude] as LatLngTuple} icon={icon}>
<Popup>
- <Link to={`/estimates/${stop.stopId}`}>{getDisplayName(stop)}</Link>
+ <Link to={`/estimates/${stop.stopId}`}>{StopDataProvider.getDisplayName(stop)}</Link>
<br />
{stop.lines.map((line) => (
<LineIcon key={line} line={line} />
diff --git a/src/pages/StopList.tsx b/src/pages/StopList.tsx
index 449ae84..a2269ec 100644
--- a/src/pages/StopList.tsx
+++ b/src/pages/StopList.tsx
@@ -37,12 +37,16 @@ export function StopList() {
}, [data])
const recentStops = useMemo(() => {
- const recent = StopDataProvider.getRecent();
-
- if (recent.length === 0) return null;
-
- return recent.map(stopId => data?.find(stop => stop.stopId === stopId) as Stop).reverse();
- }, [data])
+ // no recent items if data not loaded
+ if (!data) return null;
+ const recentIds = StopDataProvider.getRecent();
+ if (recentIds.length === 0) return null;
+ // map and filter out missing entries
+ const stopsList = recentIds
+ .map(id => data.find(stop => stop.stopId === id))
+ .filter((s): s is Stop => Boolean(s));
+ return stopsList.reverse();
+ }, [data]);
if (data === null) return <h1 className="page-title">Loading...</h1>
diff --git a/src/styles/Estimates.css b/src/styles/Estimates.css
index 1fce445..86ca09b 100644
--- a/src/styles/Estimates.css
+++ b/src/styles/Estimates.css
@@ -13,7 +13,8 @@
font-weight: 500;
}
-.table th, .table td {
+.table th,
+.table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #eee;
@@ -29,63 +30,76 @@
/* Estimates page specific styles */
.estimates-header {
- display: flex;
- align-items: center;
- margin-bottom: 1rem;
+ display: flex;
+ align-items: center;
+ margin-bottom: 1rem;
}
.estimates-stop-id {
- font-size: 1rem;
- color: var(--subtitle-color);
- margin-left: 0.5rem;
+ font-size: 1rem;
+ color: var(--subtitle-color);
+ margin-left: 0.5rem;
}
.estimates-arrival {
- color: #28a745;
- font-weight: 500;
+ color: #28a745;
+ font-weight: 500;
}
.estimates-delayed {
- color: #dc3545;
+ color: #dc3545;
}
.button-group {
- display: flex;
- gap: 1rem;
- margin-bottom: 1.5rem;
- flex-wrap: wrap;
+ display: flex;
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+ flex-wrap: wrap;
}
.button {
- padding: 0.75rem 1rem;
- background-color: var(--button-background-color);
- color: white;
- border: none;
- border-radius: 8px;
- font-size: 1rem;
- font-weight: 500;
- cursor: pointer;
- text-align: center;
- text-decoration: none;
- display: inline-block;
+ padding: 0.75rem 1rem;
+ background-color: var(--button-background-color);
+ color: white;
+ border: none;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-weight: 500;
+ cursor: pointer;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
}
.button:hover {
- background-color: var(--button-hover-background-color);
+ background-color: var(--button-hover-background-color);
}
.button:disabled {
- background-color: var(--button-disabled-background-color);
- cursor: not-allowed;
+ background-color: var(--button-disabled-background-color);
+ cursor: not-allowed;
}
.star-icon {
- margin-right: 0.5rem;
- color: #ccc;
- fill: none;
+ margin-right: 0.5rem;
+ color: #ccc;
+ fill: none;
}
.star-icon.active {
- color: var(--star-color); /* Yellow color for active star */
- fill: var(--star-color);
+ color: var(--star-color);
+ /* Yellow color for active star */
+ fill: var(--star-color);
+}
+
+/* Pencil (edit) icon next to header */
+.edit-icon {
+ margin-right: 0.5rem;
+ color: #ccc;
+ cursor: pointer;
+ stroke-width: 2px;
+}
+
+.edit-icon:hover {
+ color: var(--star-color);
} \ No newline at end of file