From 3aa6eee0f54dec3e4f92be2ad335a04145ac4db8 Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero <94913521+arielcostas@users.noreply.github.com> Date: Mon, 3 Mar 2025 18:54:35 +0100 Subject: Improve the UI --- src/pages/Estimates.tsx | 137 ++++++++++++++++++++++++++++++++++++++++++++++++ src/pages/Home.tsx | 99 ---------------------------------- src/pages/Map.tsx | 76 +++++++++++++++++++++++++++ src/pages/Stop.tsx | 131 --------------------------------------------- src/pages/StopList.tsx | 102 +++++++++++++++++++++++++++++++++++ 5 files changed, 315 insertions(+), 230 deletions(-) create mode 100644 src/pages/Estimates.tsx delete mode 100644 src/pages/Home.tsx create mode 100644 src/pages/Map.tsx delete mode 100644 src/pages/Stop.tsx create mode 100644 src/pages/StopList.tsx (limited to 'src/pages') diff --git a/src/pages/Estimates.tsx b/src/pages/Estimates.tsx new file mode 100644 index 0000000..d3b4ced --- /dev/null +++ b/src/pages/Estimates.tsx @@ -0,0 +1,137 @@ +import { useEffect, useState } from "react"; +import { Link, useParams } from "react-router"; +import { StopDataProvider } from "../data/StopDataProvider"; +import LineIcon from "../components/LineIcon"; +import { Star } from 'lucide-react'; +import "../styles/Estimates.css"; + +interface StopDetails { + stop: { + id: number; + name: string; + latitude: number; + longitude: number; + } + estimates: { + line: string; + route: string; + minutes: number; + meters: number; + }[] +} + +export function Estimates(): JSX.Element { + const sdp = new StopDataProvider(); + const [data, setData] = useState(null); + const [favourited, setFavourited] = useState(false); + const params = useParams(); + + const loadData = () => { + fetch(`/api/GetStopEstimates?id=${params.stopId}`) + .then(r => r.json()) + .then((body: StopDetails) => setData(body)); + }; + + useEffect(() => { + loadData(); + + sdp.pushRecent(parseInt(params.stopId ?? "")); + + setFavourited( + sdp.isFavourite(parseInt(params.stopId ?? "")) + ); + }, []); + + const absoluteArrivalTime = (minutes: number) => { + const now = new Date() + const arrival = new Date(now.getTime() + minutes * 60000) + return Intl.DateTimeFormat(navigator.language, { + hour: '2-digit', + minute: '2-digit' + }).format(arrival) + } + + const formatDistance = (meters: number) => { + if (meters > 1024) { + return `${(meters / 1000).toFixed(1)} km`; + } else { + return `${meters} m`; + } + } + + const toggleFavourite = () => { + if (favourited) { + sdp.removeFavourite(parseInt(params.stopId ?? "")); + setFavourited(false); + } else { + sdp.addFavourite(parseInt(params.stopId ?? "")); + setFavourited(true); + } + } + + if (data === null) return

Cargando datos en tiempo real...

+ + return ( +
+
+

+ + {data?.stop.name} ({data?.stop.id}) +

+
+ +
+ + 🔙 Volver al listado de paradas + + + +
+ +
+ + + + + + + + + + + + + + {data.estimates + .sort((a, b) => a.minutes - b.minutes) + .map((estimate, idx) => ( + + + + + + + ))} + + + {data?.estimates.length === 0 && ( + + + + + + )} +
Estimaciones de llegadas
LíneaRutaMinutosMetros
{estimate.route} + {estimate.minutes > 15 + ? absoluteArrivalTime(estimate.minutes) + : `${estimate.minutes} min`} + + {estimate.meters > -1 + ? formatDistance(estimate.meters) + : "No disponible" + } +
No hay estimaciones disponibles
+
+
+ ) +} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx deleted file mode 100644 index b7c1675..0000000 --- a/src/pages/Home.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { Link, useNavigate } from "react-router-dom"; -import { Stop, StopDataProvider } from "../data/StopDataProvider"; - -const sdp = new StopDataProvider(); - -export function Home() { - const [data, setData] = useState(null) - const navigate = useNavigate(); - - useEffect(() => { - sdp.getStops().then((stops: Stop[]) => setData(stops)) - }, []); - - const handleStopSearch = async (event: React.FormEvent) => { - event.preventDefault() - - const stopId = (event.target as HTMLFormElement).stopId.value - const searchNumber = parseInt(stopId) - if (data?.find(stop => stop.stopId === searchNumber)) { - navigate(`/${searchNumber}`) - } else { - alert("Parada no encontrada") - } - } - - const favouritedStops = useMemo(() => { - return data?.filter(stop => stop.favourite) ?? [] - }, [data]) - - const recentStops = useMemo(() => { - const recent = sdp.getRecent(); - - if (recent.length === 0) return null; - - return recent.map(stopId => data?.find(stop => stop.stopId === stopId) as Stop).reverse(); - }, [data]) - - if (data === null) return

Loading...

- - return ( - <> -

UrbanoVigo Web

- -
-
- - -
- - -
- -

Paradas favoritas

- - {favouritedStops?.length == 1 && ( -

- Accede a una parada y márcala como favorita para verla aquí. -

- )} - - - -

Recientes

- - - -

Paradas

- - - - ) -} diff --git a/src/pages/Map.tsx b/src/pages/Map.tsx new file mode 100644 index 0000000..dbf5b9f --- /dev/null +++ b/src/pages/Map.tsx @@ -0,0 +1,76 @@ +import { useEffect, useMemo, useState } from "react"; +import { Link, useNavigate } from "react-router"; +import { Stop, StopDataProvider } from "../data/StopDataProvider"; +import LineIcon from "../components/LineIcon"; + +const sdp = new StopDataProvider(); + +export function StopMap() { + const [data, setData] = useState(null) + const navigate = useNavigate(); + + useEffect(() => { + sdp.getStops().then((stops: Stop[]) => setData(stops)) + }, []); + + const handleStopSearch = async (event: React.FormEvent) => { + event.preventDefault() + + const stopId = (event.target as HTMLFormElement).stopId.value + const searchNumber = parseInt(stopId) + if (data?.find(stop => stop.stopId === searchNumber)) { + navigate(`/estimates/${searchNumber}`) + } else { + alert("Parada no encontrada") + } + } + + if (data === null) return

Loading...

+ + return ( +
+

Map View

+ +
+ {/* Map placeholder - in a real implementation, this would be a map component */} +
+

Map will be displayed here

+
+
+ +
+
+ + +
+ + +
+ +
+

Nearby Stops

+
    + {data?.slice(0, 5).map((stop: Stop) => ( +
  • + + ({stop.stopId}) {stop.name} +
    + {stop.lines?.map(line => )} +
    + +
  • + ))} +
+
+
+ ) +} diff --git a/src/pages/Stop.tsx b/src/pages/Stop.tsx deleted file mode 100644 index aa6651c..0000000 --- a/src/pages/Stop.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { useEffect, useState } from "react"; -import { Link, useParams } from "react-router-dom"; -import { StopDataProvider } from "../data/StopDataProvider"; - -interface StopDetails { - stop: { - id: number; - name: string; - latitude: number; - longitude: number; - } - estimates: { - line: string; - route: string; - minutes: number; - meters: number; - }[] -} - -export function Stop(): JSX.Element { - const sdp = new StopDataProvider(); - const [data, setData] = useState(null); - const [favourited, setFavourited] = useState(false); - const params = useParams(); - - const loadData = () => { - fetch(`/api/GetStopEstimates?id=${params.stopId}`) - .then(r => r.json()) - .then((body: StopDetails) => setData(body)); - }; - - useEffect(() => { - loadData(); - - sdp.pushRecent(parseInt(params.stopId ?? "")); - - setFavourited( - sdp.isFavourite(parseInt(params.stopId ?? "")) - ); - }) - - const absoluteArrivalTime = (minutes: number) => { - const now = new Date() - const arrival = new Date(now.getTime() + minutes * 60000) - return Intl.DateTimeFormat(navigator.language, { - hour: '2-digit', - minute: '2-digit' - }).format(arrival) - } - - if (data === null) return

Cargando datos en tiempo real...

- - return ( - <> -
-

{data?.stop.name} ({data?.stop.id})

-
- -
- - 🔙 Volver al listado de paradas - - - {!favourited && ( - - )} - - {favourited && ( - - )} - - -
- - - - - - - - - - - - - - - {data.estimates - .sort((a, b) => a.minutes - b.minutes) - .map((estimate, idx) => ( - - - - - - - ))} - - - {data?.estimates.length === 0 && ( - - - - - - )} -
Estimaciones de llegadas
LíneaRutaMinutosMetros
{estimate.line}{estimate.route} - {estimate.minutes} ({absoluteArrivalTime(estimate.minutes)}) - - {estimate.meters > -1 - ? `${estimate.meters} metros` - : "No disponible" - } -
No hay estimaciones disponibles
- -

- Volver al inicio -

- - ) -} diff --git a/src/pages/StopList.tsx b/src/pages/StopList.tsx new file mode 100644 index 0000000..2351b51 --- /dev/null +++ b/src/pages/StopList.tsx @@ -0,0 +1,102 @@ +import { useEffect, useMemo, useState } from "react"; +import { Stop, StopDataProvider } from "../data/StopDataProvider"; +import StopItem from "../components/StopItem"; +import Fuse from "fuse.js"; + +const sdp = new StopDataProvider(); + +export function StopList() { + const [data, setData] = useState(null) + const [searchResults, setSearchResults] = useState(null); + + useEffect(() => { + sdp.getStops().then((stops: Stop[]) => setData(stops)) + }, []); + + const handleStopSearch = (event: React.ChangeEvent) => { + const stopName = event.target.value; + if (data) { + const fuse = new Fuse(data, { keys: ['name'], threshold: 0.3 }); + const results = fuse.search(stopName).map(result => result.item); + setSearchResults(results); + } + } + + const favouritedStops = useMemo(() => { + return data?.filter(stop => stop.favourite) ?? [] + }, [data]) + + const recentStops = useMemo(() => { + const recent = sdp.getRecent(); + + if (recent.length === 0) return null; + + return recent.map(stopId => data?.find(stop => stop.stopId === stopId) as Stop).reverse(); + }, [data]) + + if (data === null) return

Loading...

+ + return ( +
+

UrbanoVigo Web

+ +
+
+ + +
+
+ + {searchResults && searchResults.length > 0 && ( +
+

Resultados de la búsqueda

+
    + {searchResults.map((stop: Stop) => ( + + ))} +
+
+ )} + +
+

Paradas favoritas

+ + {favouritedStops?.length === 0 && ( +

+ Accede a una parada y márcala como favorita para verla aquí. +

+ )} + +
    + {favouritedStops?.sort((a, b) => a.stopId - b.stopId).map((stop: Stop) => ( + + ))} +
+
+ + {recentStops && recentStops.length > 0 && ( +
+

Recientes

+ +
    + {recentStops.map((stop: Stop) => ( + + ))} +
+
+ )} + +
+

Paradas

+ +
    + {data?.sort((a, b) => a.stopId - b.stopId).map((stop: Stop) => ( + + ))} +
+
+
+ ) +} -- cgit v1.3