diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-04-20 20:15:55 +0200 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-04-20 20:15:55 +0200 |
| commit | 3676b1d1d9216a676c7d5a40affa5b3256ca8df3 (patch) | |
| tree | efa63a0d21ae52e32e405fe7b4ce56b02d782e86 /src | |
| parent | c86b4655f72c86362c064dd50bb701782b39e6eb (diff) | |
Refactor stop data handling with caching and custom names support
Diffstat (limited to 'src')
| -rw-r--r-- | src/data/StopDataProvider.ts | 101 | ||||
| -rw-r--r-- | src/pages/Estimates.tsx | 28 | ||||
| -rw-r--r-- | src/pages/Map.tsx | 14 | ||||
| -rw-r--r-- | src/pages/StopList.tsx | 16 | ||||
| -rw-r--r-- | src/styles/Estimates.css | 80 |
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 |
