aboutsummaryrefslogtreecommitdiff
path: root/src/pages
diff options
context:
space:
mode:
authorAriel Costas Guerrero <94913521+arielcostas@users.noreply.github.com>2025-03-03 18:54:35 +0100
committerAriel Costas Guerrero <94913521+arielcostas@users.noreply.github.com>2025-03-03 18:54:35 +0100
commit3aa6eee0f54dec3e4f92be2ad335a04145ac4db8 (patch)
tree9ccffabd2972249322ebaa6d1de26289d7a41a4c /src/pages
parentd3726e50167ed07c483c542cf6739f103dda0dd5 (diff)
Improve the UI
Diffstat (limited to 'src/pages')
-rw-r--r--src/pages/Estimates.tsx137
-rw-r--r--src/pages/Home.tsx99
-rw-r--r--src/pages/Map.tsx76
-rw-r--r--src/pages/Stop.tsx131
-rw-r--r--src/pages/StopList.tsx102
5 files changed, 315 insertions, 230 deletions
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<StopDetails | null>(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 <h1 className="page-title">Cargando datos en tiempo real...</h1>
+
+ return (
+ <div className="page-container">
+ <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>
+ </h1>
+ </div>
+
+ <div className="button-group">
+ <Link to="/stops" className="button">
+ 🔙 Volver al listado de paradas
+ </Link>
+
+ <button className="button" onClick={loadData}>⬇️ Recargar</button>
+ </div>
+
+ <div className="table-responsive">
+ <table className="table">
+ <caption>Estimaciones de llegadas</caption>
+
+ <thead>
+ <tr>
+ <th>Línea</th>
+ <th>Ruta</th>
+ <th>Minutos</th>
+ <th>Metros</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ {data.estimates
+ .sort((a, b) => a.minutes - b.minutes)
+ .map((estimate, idx) => (
+ <tr key={idx}>
+ <td><LineIcon line={estimate.line} /></td>
+ <td>{estimate.route}</td>
+ <td>
+ {estimate.minutes > 15
+ ? absoluteArrivalTime(estimate.minutes)
+ : `${estimate.minutes} min`}
+ </td>
+ <td>
+ {estimate.meters > -1
+ ? formatDistance(estimate.meters)
+ : "No disponible"
+ }
+ </td>
+ </tr>
+ ))}
+ </tbody>
+
+ {data?.estimates.length === 0 && (
+ <tfoot>
+ <tr>
+ <td colSpan={4}>No hay estimaciones disponibles</td>
+ </tr>
+ </tfoot>
+ )}
+ </table>
+ </div>
+ </div>
+ )
+}
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<Stop[] | null>(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 <h1>Loading...</h1>
-
- return (
- <>
- <h1>UrbanoVigo Web</h1>
-
- <form action="none" onSubmit={handleStopSearch}>
- <div>
- <label htmlFor="stopId">
- ID
- </label>
- <input type="number" placeholder="ID de parada" id="stopId" />
- </div>
-
- <button type="submit">Buscar</button>
- </form>
-
- <h2>Paradas favoritas</h2>
-
- {favouritedStops?.length == 1 && (
- <p>
- Accede a una parada y márcala como favorita para verla aquí.
- </p>
- )}
-
- <ul>
- {favouritedStops?.sort((a, b) => a.stopId - b.stopId).map((stop: Stop) => (
- <li key={stop.stopId}>
- <Link to={`/${stop.stopId}`}>
- ({stop.stopId}) {stop.name} - {stop.lines?.join(', ')}
- </Link>
- </li>
- ))}
- </ul>
-
- <h2>Recientes</h2>
-
- <ul>
- {recentStops?.map((stop: Stop) => (
- <li key={stop.stopId}>
- <Link to={`/${stop.stopId}`}>
- ({stop.stopId}) {stop.name} - {stop.lines?.join(', ')}
- </Link>
- </li>
- ))}
- </ul>
-
- <h2>Paradas</h2>
-
- <ul>
- {data?.sort((a, b) => a.stopId - b.stopId).map((stop: Stop) => (
- <li key={stop.stopId}>
- <Link to={`/${stop.stopId}`}>
- ({stop.stopId}) {stop.name} - {stop.lines?.join(', ')}
- </Link>
- </li>
- ))}
- </ul>
- </>
- )
-}
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<Stop[] | null>(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 <h1 className="page-title">Loading...</h1>
+
+ return (
+ <div className="page-container">
+ <h1 className="page-title">Map View</h1>
+
+ <div className="map-container">
+ {/* Map placeholder - in a real implementation, this would be a map component */}
+ <div style={{
+ height: '100%',
+ backgroundColor: '#f0f0f0',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderRadius: '8px'
+ }}>
+ <p>Map will be displayed here</p>
+ </div>
+ </div>
+
+ <form className="search-form" onSubmit={handleStopSearch}>
+ <div className="form-group">
+ <label className="form-label" htmlFor="stopId">
+ Find Stop by ID
+ </label>
+ <input className="form-input" type="number" placeholder="Stop ID" id="stopId" />
+ </div>
+
+ <button className="form-button" type="submit">Search</button>
+ </form>
+
+ <div className="list-container">
+ <h2 className="page-subtitle">Nearby Stops</h2>
+ <ul className="list">
+ {data?.slice(0, 5).map((stop: Stop) => (
+ <li className="list-item" key={stop.stopId}>
+ <Link className="list-item-link" to={`/estimates/${stop.stopId}`}>
+ ({stop.stopId}) {stop.name}
+ <div className="line-icons">
+ {stop.lines?.map(line => <LineIcon key={line} line={line} />)}
+ </div>
+ </Link>
+ </li>
+ ))}
+ </ul>
+ </div>
+ </div>
+ )
+}
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<StopDetails | null>(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 <h1>Cargando datos en tiempo real...</h1>
-
- return (
- <>
- <div>
- <h1>{data?.stop.name} ({data?.stop.id})</h1>
- </div>
-
- <div style={{display: 'flex', gap: '1rem'}}>
- <Link to="/" className="button">
- 🔙 Volver al listado de paradas
- </Link>
-
- {!favourited && (
- <button type="button" onClick={() => {
- sdp.addFavourite(parseInt(params.stopId ?? ""));
- setFavourited(true);
- }}>
- ⭐ Añadir a favoritos
- </button>
- )}
-
- {favourited && (
- <button type="button" onClick={() => {
- sdp.removeFavourite(parseInt(params.stopId ?? ""));
- setFavourited(false);
- }}>
- ⭐Quitar de favoritos
- </button>
- )}
-
- <button onClick={loadData}>⬇️ Recargar</button>
- </div>
-
- <table>
- <caption>Estimaciones de llegadas</caption>
-
- <thead>
- <tr>
- <th>Línea</th>
- <th>Ruta</th>
- <th>Minutos</th>
- <th>Metros</th>
- </tr>
- </thead>
-
- <tbody>
- {data.estimates
- .sort((a, b) => a.minutes - b.minutes)
- .map((estimate, idx) => (
- <tr key={idx}>
- <td>{estimate.line}</td>
- <td>{estimate.route}</td>
- <td>
- {estimate.minutes} ({absoluteArrivalTime(estimate.minutes)})
- </td>
- <td>
- {estimate.meters > -1
- ? `${estimate.meters} metros`
- : "No disponible"
- }
- </td>
- </tr>
- ))}
- </tbody>
-
- {data?.estimates.length === 0 && (
- <tfoot>
- <tr>
- <td colSpan={4}>No hay estimaciones disponibles</td>
- </tr>
- </tfoot>
- )}
- </table>
-
- <p>
- <Link to="/">Volver al inicio</Link>
- </p>
- </>
- )
-}
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<Stop[] | null>(null)
+ const [searchResults, setSearchResults] = useState<Stop[] | null>(null);
+
+ useEffect(() => {
+ sdp.getStops().then((stops: Stop[]) => setData(stops))
+ }, []);
+
+ const handleStopSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
+ 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 <h1 className="page-title">Loading...</h1>
+
+ return (
+ <div className="page-container">
+ <h1 className="page-title">UrbanoVigo Web</h1>
+
+ <form className="search-form">
+ <div className="form-group">
+ <label className="form-label" htmlFor="stopName">
+ Nombre de la parada
+ </label>
+ <input className="form-input" type="text" placeholder="Nombre de la parada" id="stopName" onChange={handleStopSearch} />
+ </div>
+ </form>
+
+ {searchResults && searchResults.length > 0 && (
+ <div className="list-container">
+ <h2 className="page-subtitle">Resultados de la búsqueda</h2>
+ <ul className="list">
+ {searchResults.map((stop: Stop) => (
+ <StopItem key={stop.stopId} stop={stop} />
+ ))}
+ </ul>
+ </div>
+ )}
+
+ <div className="list-container">
+ <h2 className="page-subtitle">Paradas favoritas</h2>
+
+ {favouritedStops?.length === 0 && (
+ <p className="message">
+ Accede a una parada y márcala como favorita para verla aquí.
+ </p>
+ )}
+
+ <ul className="list">
+ {favouritedStops?.sort((a, b) => a.stopId - b.stopId).map((stop: Stop) => (
+ <StopItem key={stop.stopId} stop={stop} />
+ ))}
+ </ul>
+ </div>
+
+ {recentStops && recentStops.length > 0 && (
+ <div className="list-container">
+ <h2 className="page-subtitle">Recientes</h2>
+
+ <ul className="list">
+ {recentStops.map((stop: Stop) => (
+ <StopItem key={stop.stopId} stop={stop} />
+ ))}
+ </ul>
+ </div>
+ )}
+
+ <div className="list-container">
+ <h2 className="page-subtitle">Paradas</h2>
+
+ <ul className="list">
+ {data?.sort((a, b) => a.stopId - b.stopId).map((stop: Stop) => (
+ <StopItem key={stop.stopId} stop={stop} />
+ ))}
+ </ul>
+ </div>
+ </div>
+ )
+}