diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-04-20 19:07:29 +0200 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-04-20 19:07:29 +0200 |
| commit | 340865ad234f974ec4c0afcbfb6ff06f50a11b2f (patch) | |
| tree | 06b6a76b6dfc535f9091990eaaec82b50d5f851e /src | |
| parent | 32bc4914759845516de2b7592b05afb3c3cc5c42 (diff) | |
Load stops from local data files
Diffstat (limited to 'src')
| -rw-r--r-- | src/AppContext.tsx | 2 | ||||
| -rw-r--r-- | src/components/StopItem.css | 54 | ||||
| -rw-r--r-- | src/components/StopItem.tsx | 6 | ||||
| -rw-r--r-- | src/data/StopDataProvider.ts | 150 | ||||
| -rw-r--r-- | src/pages/Estimates.tsx | 12 | ||||
| -rw-r--r-- | src/pages/Map.tsx | 16 | ||||
| -rw-r--r-- | src/pages/StopList.tsx | 8 |
7 files changed, 164 insertions, 84 deletions
diff --git a/src/AppContext.tsx b/src/AppContext.tsx index 4ff391b..a9af208 100644 --- a/src/AppContext.tsx +++ b/src/AppContext.tsx @@ -155,7 +155,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => { ); } } - }, []); + }, [mapState.hasLocationPermission, mapState.userLocation]); return ( <AppContext.Provider value={{ diff --git a/src/components/StopItem.css b/src/components/StopItem.css new file mode 100644 index 0000000..9feb2d1 --- /dev/null +++ b/src/components/StopItem.css @@ -0,0 +1,54 @@ +/* Stop Item Styling */ + +.stop-notes { + font-size: 0.85rem; + font-style: italic; + color: #666; + margin: 2px 0; +} + +.stop-amenities { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 4px; +} + +.amenity-tag { + font-size: 0.75rem; + background-color: #e8f4f8; + color: #0078d4; + border-radius: 4px; + padding: 2px 6px; + display: inline-block; +} + +/* Different colors for different amenity types */ +.amenity-tag[data-amenity="shelter"] { + background-color: #e3f1df; + color: #107c41; +} + +.amenity-tag[data-amenity="bench"] { + background-color: #f0e8fc; + color: #5c2e91; +} + +.amenity-tag[data-amenity="real-time display"] { + background-color: #fff4ce; + color: #986f0b; +} + +/* When there are alternate names available, show an indicator */ +.has-alternate-names { + position: relative; +} + +.has-alternate-names::after { + content: "⋯"; + position: absolute; + right: -15px; + top: 0; + color: #0078d4; + font-weight: bold; +}
\ No newline at end of file diff --git a/src/components/StopItem.tsx b/src/components/StopItem.tsx index 6b48899..cf9ccfc 100644 --- a/src/components/StopItem.tsx +++ b/src/components/StopItem.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Link } from 'react-router'; -import { Stop } from '../data/StopDataProvider'; +import StopDataProvider, { Stop } from '../data/StopDataProvider'; import LineIcon from './LineIcon'; interface StopItemProps { @@ -8,13 +8,15 @@ interface StopItemProps { } const StopItem: React.FC<StopItemProps> = ({ stop }) => { + return ( <li className="list-item"> <Link className="list-item-link" to={`/estimates/${stop.stopId}`}> - ({stop.stopId}) {stop.name} + {stop.favourite && <span className="favourite-icon">★</span>} ({stop.stopId}) {StopDataProvider.getDisplayName(stop)} <div className="line-icons"> {stop.lines?.map(line => <LineIcon key={line} line={line} />)} </div> + </Link> </li> ); diff --git a/src/data/StopDataProvider.ts b/src/data/StopDataProvider.ts index 11674fd..55d0e78 100644 --- a/src/data/StopDataProvider.ts +++ b/src/data/StopDataProvider.ts @@ -3,91 +3,113 @@ export interface CachedStopList { data: Stop[]; } +export type StopName = { + original: string; + intersect?: string; +} + export interface Stop { - stopId: number - name: string; + stopId: number; + name: StopName; latitude?: number; longitude?: number; lines: string[]; favourite?: boolean; } -export class StopDataProvider { - async getStops(): Promise<Stop[]> { - const rawFavouriteStops = localStorage.getItem('favouriteStops'); - let favouriteStops: number[] = []; - if (rawFavouriteStops) { - favouriteStops = JSON.parse(rawFavouriteStops) as number[]; - } - - const response = await fetch('/api/GetStopList'); - const stops = await response.json() as Stop[]; +export default { + getStops, + getDisplayName, + addFavourite, + removeFavourite, + isFavourite, + pushRecent, + getRecent +}; - return stops.map((stop: Stop) => { - return { - ...stop, - favourite: favouriteStops.includes(stop.stopId) - }; - }); +async function getStops(): Promise<Stop[]> { + const rawFavouriteStops = localStorage.getItem('favouriteStops'); + let favouriteStops: number[] = []; + if (rawFavouriteStops) { + favouriteStops = JSON.parse(rawFavouriteStops) as number[]; } - addFavourite(stopId: number) { - const rawFavouriteStops = localStorage.getItem('favouriteStops'); - let favouriteStops: number[] = []; - if (rawFavouriteStops) { - favouriteStops = JSON.parse(rawFavouriteStops) as number[]; - } + const response = await fetch('/stops.json'); + const stops = await response.json() as Stop[]; + + return stops.map((stop: Stop) => { + return { + ...stop, + favourite: favouriteStops.includes(stop.stopId) + }; + }); +} - if (!favouriteStops.includes(stopId)) { - favouriteStops.push(stopId); - localStorage.setItem('favouriteStops', JSON.stringify(favouriteStops)); - } +// Get display name based on preferences or context +function getDisplayName(stop: Stop): string { + if (typeof stop.name === 'string') { + return stop.name; } - removeFavourite(stopId: number) { - const rawFavouriteStops = localStorage.getItem('favouriteStops'); - let favouriteStops: number[] = []; - if (rawFavouriteStops) { - favouriteStops = JSON.parse(rawFavouriteStops) as number[]; - } + return stop.name.intersect || stop.name.original; +} - const newFavouriteStops = favouriteStops.filter(id => id !== stopId); - localStorage.setItem('favouriteStops', JSON.stringify(newFavouriteStops)); +function addFavourite(stopId: number) { + const rawFavouriteStops = localStorage.getItem('favouriteStops'); + let favouriteStops: number[] = []; + if (rawFavouriteStops) { + favouriteStops = JSON.parse(rawFavouriteStops) as number[]; } - isFavourite(stopId: number): boolean { - const rawFavouriteStops = localStorage.getItem('favouriteStops'); - if (rawFavouriteStops) { - const favouriteStops = JSON.parse(rawFavouriteStops) as number[]; - return favouriteStops.includes(stopId); - } - return false; + if (!favouriteStops.includes(stopId)) { + favouriteStops.push(stopId); + localStorage.setItem('favouriteStops', JSON.stringify(favouriteStops)); } +} - RECENT_STOPS_LIMIT = 10; +function removeFavourite(stopId: number) { + const rawFavouriteStops = localStorage.getItem('favouriteStops'); + let favouriteStops: number[] = []; + if (rawFavouriteStops) { + favouriteStops = JSON.parse(rawFavouriteStops) as number[]; + } - pushRecent(stopId: number) { - const rawRecentStops = localStorage.getItem('recentStops'); - let recentStops: Set<number> = new Set(); - if (rawRecentStops) { - recentStops = new Set(JSON.parse(rawRecentStops) as number[]); - } + const newFavouriteStops = favouriteStops.filter(id => id !== stopId); + localStorage.setItem('favouriteStops', JSON.stringify(newFavouriteStops)); +} - recentStops.add(stopId); - if (recentStops.size > this.RECENT_STOPS_LIMIT) { - const iterator = recentStops.values(); - const val = iterator.next().value as number; - recentStops.delete(val); - } +function isFavourite(stopId: number): boolean { + const rawFavouriteStops = localStorage.getItem('favouriteStops'); + if (rawFavouriteStops) { + const favouriteStops = JSON.parse(rawFavouriteStops) as number[]; + return favouriteStops.includes(stopId); + } + return false; +} + +const RECENT_STOPS_LIMIT = 10; + +function pushRecent(stopId: number) { + const rawRecentStops = localStorage.getItem('recentStops'); + let recentStops: Set<number> = new Set(); + if (rawRecentStops) { + recentStops = new Set(JSON.parse(rawRecentStops) as number[]); + } - localStorage.setItem('recentStops', JSON.stringify(Array.from(recentStops))); + recentStops.add(stopId); + if (recentStops.size > RECENT_STOPS_LIMIT) { + const iterator = recentStops.values(); + const val = iterator.next().value as number; + recentStops.delete(val); } - getRecent(): number[] { - const rawRecentStops = localStorage.getItem('recentStops'); - if (rawRecentStops) { - return JSON.parse(rawRecentStops) as number[]; - } - return []; + localStorage.setItem('recentStops', JSON.stringify(Array.from(recentStops))); +} + +function getRecent(): number[] { + const rawRecentStops = localStorage.getItem('recentStops'); + if (rawRecentStops) { + return JSON.parse(rawRecentStops) as number[]; } -}
\ No newline at end of file + return []; +} diff --git a/src/pages/Estimates.tsx b/src/pages/Estimates.tsx index 900ffc5..90745da 100644 --- a/src/pages/Estimates.tsx +++ b/src/pages/Estimates.tsx @@ -1,6 +1,6 @@ import { JSX, useEffect, useState } from "react"; import { useParams } from "react-router"; -import { StopDataProvider } from "../data/StopDataProvider"; +import StopDataProvider from "../data/StopDataProvider"; import { Star } from 'lucide-react'; import "../styles/Estimates.css"; import { RegularTable } from "../components/RegularTable"; @@ -22,8 +22,6 @@ export interface StopDetails { }[] } -const sdp = new StopDataProvider(); - const loadData = async (stopId: string) => { const resp = await fetch(`/api/GetStopEstimates?id=${stopId}`); return await resp.json(); @@ -44,20 +42,20 @@ export function Estimates(): JSX.Element { }) - sdp.pushRecent(parseInt(params.stopId ?? "")); + StopDataProvider.pushRecent(parseInt(params.stopId ?? "")); setFavourited( - sdp.isFavourite(parseInt(params.stopId ?? "")) + StopDataProvider.isFavourite(parseInt(params.stopId ?? "")) ); }, [params.stopId]); const toggleFavourite = () => { if (favourited) { - sdp.removeFavourite(parseInt(params.stopId ?? "")); + StopDataProvider.removeFavourite(parseInt(params.stopId ?? "")); setFavourited(false); } else { - sdp.addFavourite(parseInt(params.stopId ?? "")); + StopDataProvider.addFavourite(parseInt(params.stopId ?? "")); setFavourited(true); } } diff --git a/src/pages/Map.tsx b/src/pages/Map.tsx index 9abb7a3..af95bf9 100644 --- a/src/pages/Map.tsx +++ b/src/pages/Map.tsx @@ -1,4 +1,4 @@ -import { StopDataProvider, Stop } from "../data/StopDataProvider"; +import StopDataProvider, { Stop } from "../data/StopDataProvider"; import 'leaflet/dist/leaflet.css' import 'react-leaflet-markercluster/styles' @@ -20,8 +20,6 @@ const icon = new Icon({ shadowSize: [41, 41] }); -const sdp = new StopDataProvider(); - // Componente auxiliar para detectar cambios en el mapa const MapEventHandler = () => { const { updateMapState } = useApp(); @@ -43,9 +41,17 @@ export function StopMap() { const { mapState } = useApp(); useEffect(() => { - sdp.getStops().then((stops) => { setStops(stops); }); + StopDataProvider.getStops().then((stops) => { setStops(stops); }); }, []); + 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} @@ -63,7 +69,7 @@ export function StopMap() { {stops.map((stop) => ( <Marker key={stop.stopId} position={[stop.latitude, stop.longitude] as LatLngTuple} icon={icon}> <Popup> - <Link to={`/estimates/${stop.stopId}`}>{stop.name}</Link> + <Link to={`/estimates/${stop.stopId}`}>{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 72cccda..449ae84 100644 --- a/src/pages/StopList.tsx +++ b/src/pages/StopList.tsx @@ -1,10 +1,8 @@ import { useEffect, useMemo, useState } from "react"; -import { Stop, StopDataProvider } from "../data/StopDataProvider"; +import StopDataProvider, { Stop } from "../data/StopDataProvider"; import StopItem from "../components/StopItem"; import Fuse from "fuse.js"; -const sdp = new StopDataProvider(); - const placeholders = [ "Urzaiz", "Gran Vía", @@ -22,7 +20,7 @@ export function StopList() { const [searchResults, setSearchResults] = useState<Stop[] | null>(null); useEffect(() => { - sdp.getStops().then((stops: Stop[]) => setData(stops)) + StopDataProvider.getStops().then((stops: Stop[]) => setData(stops)) }, []); const handleStopSearch = (event: React.ChangeEvent<HTMLInputElement>) => { @@ -39,7 +37,7 @@ export function StopList() { }, [data]) const recentStops = useMemo(() => { - const recent = sdp.getRecent(); + const recent = StopDataProvider.getRecent(); if (recent.length === 0) return null; |
