diff options
Diffstat (limited to 'src/pages')
| -rw-r--r-- | src/pages/Estimates.tsx | 137 | ||||
| -rw-r--r-- | src/pages/Home.tsx | 99 | ||||
| -rw-r--r-- | src/pages/Map.tsx | 76 | ||||
| -rw-r--r-- | src/pages/Stop.tsx | 131 | ||||
| -rw-r--r-- | src/pages/StopList.tsx | 102 |
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> + ) +} |
