aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/AppContext.tsx2
-rw-r--r--src/components/StopItem.css54
-rw-r--r--src/components/StopItem.tsx6
-rw-r--r--src/data/StopDataProvider.ts150
-rw-r--r--src/pages/Estimates.tsx12
-rw-r--r--src/pages/Map.tsx16
-rw-r--r--src/pages/StopList.tsx8
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;