diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-05-26 10:48:43 +0200 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-05-26 10:48:43 +0200 |
| commit | 5ced7f916d94e86e9a7ec164bee56f9a8e3a2a3a (patch) | |
| tree | b1ef5afa17b4a2f9fb2cbd683afc2fb6d905b5e1 /src/pages | |
| parent | 4637373b50636e78dc2c7b6f99be879edb4ff7dc (diff) | |
Replace Azure SWA with custom server
Diffstat (limited to 'src/pages')
| -rw-r--r-- | src/pages/Estimates.tsx | 99 | ||||
| -rw-r--r-- | src/pages/Map.tsx | 75 | ||||
| -rw-r--r-- | src/pages/Settings.tsx | 65 | ||||
| -rw-r--r-- | src/pages/StopList.tsx | 135 |
4 files changed, 0 insertions, 374 deletions
diff --git a/src/pages/Estimates.tsx b/src/pages/Estimates.tsx deleted file mode 100644 index 7cf941a..0000000 --- a/src/pages/Estimates.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { JSX, useEffect, useState } from "react"; -import { useParams } from "react-router"; -import StopDataProvider from "../data/StopDataProvider"; -import { Star, Edit2 } from 'lucide-react'; -import "../styles/Estimates.css"; -import { RegularTable } from "../components/RegularTable"; -import { useApp } from "../AppContext"; -import { GroupedTable } from "../components/GroupedTable"; - -export interface StopDetails { - stop: { - id: number; - name: string; - latitude: number; - longitude: number; - } - estimates: { - line: string; - route: string; - minutes: number; - meters: number; - }[] -} - -const loadData = async (stopId: string) => { - const resp = await fetch(`/api/GetStopEstimates?id=${stopId}`); - return await resp.json(); -}; - -export function Estimates(): JSX.Element { - const params = useParams(); - const stopIdNum = parseInt(params.stopId ?? ""); - const [customName, setCustomName] = useState<string | undefined>(undefined); - const [data, setData] = useState<StopDetails | null>(null); - const [dataDate, setDataDate] = useState<Date | null>(null); - const [favourited, setFavourited] = useState(false); - const { tableStyle } = useApp(); - - useEffect(() => { - loadData(params.stopId!) - .then((body: StopDetails) => { - setData(body); - setDataDate(new Date()); - setCustomName(StopDataProvider.getCustomName(stopIdNum)); - }) - - - StopDataProvider.pushRecent(parseInt(params.stopId ?? "")); - - setFavourited( - StopDataProvider.isFavourite(parseInt(params.stopId ?? "")) - ); - }, [params.stopId]); - - - const toggleFavourite = () => { - if (favourited) { - StopDataProvider.removeFavourite(stopIdNum); - setFavourited(false); - } else { - StopDataProvider.addFavourite(stopIdNum); - setFavourited(true); - } - } - - const handleRename = () => { - const current = customName ?? data?.stop.name; - const input = window.prompt('Custom name for this stop:', current); - if (input === null) return; // cancelled - const trimmed = input.trim(); - if (trimmed === '') { - StopDataProvider.removeCustomName(stopIdNum); - setCustomName(undefined); - } else { - StopDataProvider.setCustomName(stopIdNum, trimmed); - setCustomName(trimmed); - } - }; - - 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} /> - <Edit2 className="edit-icon" onClick={handleRename} /> - {(customName ?? data.stop.name)} <span className="estimates-stop-id">({data.stop.id})</span> - </h1> - </div> - - <div className="table-responsive"> - {tableStyle === 'grouped' ? - <GroupedTable data={data} dataDate={dataDate} /> : - <RegularTable data={data} dataDate={dataDate} />} - </div> - </div> - ) -} diff --git a/src/pages/Map.tsx b/src/pages/Map.tsx deleted file mode 100644 index 1f0a9e0..0000000 --- a/src/pages/Map.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import StopDataProvider, { Stop } from "../data/StopDataProvider"; - -import 'leaflet/dist/leaflet.css' -import 'react-leaflet-markercluster/styles' - -import { useEffect, useState } from 'react'; -import LineIcon from '../components/LineIcon'; -import { Link } from 'react-router'; -import { MapContainer, TileLayer, Marker, Popup, useMapEvents } from "react-leaflet"; -import MarkerClusterGroup from "react-leaflet-markercluster"; -import { Icon, LatLngTuple } from "leaflet"; -import { EnhancedLocateControl } from "../controls/LocateControl"; -import { useApp } from "../AppContext"; - -const icon = new Icon({ - iconUrl: '/map-pin-icon.png', - iconSize: [25, 41], - iconAnchor: [12, 41], - popupAnchor: [1, -34], - shadowSize: [41, 41] -}); - -// Componente auxiliar para detectar cambios en el mapa -const MapEventHandler = () => { - const { updateMapState } = useApp(); - - const map = useMapEvents({ - moveend: () => { - const center = map.getCenter(); - const zoom = map.getZoom(); - updateMapState([center.lat, center.lng], zoom); - } - }); - - return null; -}; - -// Componente principal del mapa -export function StopMap() { - const [stops, setStops] = useState<Stop[]>([]); - const { mapState } = useApp(); - - useEffect(() => { - StopDataProvider.getStops().then(setStops); - }, []); - - return ( - <MapContainer - center={mapState.center} - zoom={mapState.zoom} - scrollWheelZoom={true} - style={{ height: '100%' }} - > - <TileLayer - attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a>, © <a href="https://carto.com/attributions">CARTO</a>' - url="https://d.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png" - /> - <EnhancedLocateControl /> - <MapEventHandler /> - <MarkerClusterGroup> - {stops.map(stop => ( - <Marker key={stop.stopId} position={[stop.latitude, stop.longitude] as LatLngTuple} icon={icon}> - <Popup> - <Link to={`/estimates/${stop.stopId}`}>{StopDataProvider.getDisplayName(stop)}</Link> - <br /> - {stop.lines.map((line) => ( - <LineIcon key={line} line={line} /> - ))} - </Popup> - </Marker> - ))} - </MarkerClusterGroup> - </MapContainer> - ); -} diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx deleted file mode 100644 index 1ad15ab..0000000 --- a/src/pages/Settings.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useApp } from "../AppContext"; -import "../styles/Settings.css"; - -export function Settings() { - const { theme, setTheme, tableStyle, setTableStyle, mapPositionMode, setMapPositionMode } = useApp(); - - return ( - <div className="about-page"> - <h1 className="page-title">Sobre UrbanoVigo Web</h1> - <p className="about-description"> - Aplicación web para encontrar paradas y tiempos de llegada de los autobuses - urbanos de Vigo, España. - </p> - <section className="settings-section"> - <h2>Ajustes</h2> - <div className="settings-content-inline"> - <label htmlFor="theme" className="form-label-inline">Modo:</label> - <select id="theme" className="form-select-inline" value={theme} onChange={(e) => setTheme(e.target.value as "light" | "dark")}> - <option value="light">Claro</option> - <option value="dark">Oscuro</option> - </select> - </div> - <div className="settings-content-inline"> - <label htmlFor="tableStyle" className="form-label-inline">Estilo de tabla:</label> - <select id="tableStyle" className="form-select-inline" value={tableStyle} onChange={(e) => setTableStyle(e.target.value as "regular" | "grouped")}> - <option value="regular">Mostrar por orden</option> - <option value="grouped">Agrupar por línea</option> - </select> - </div> - <div className="settings-content-inline"> - <label htmlFor="mapPositionMode" className="form-label-inline">Posición del mapa:</label> - <select id="mapPositionMode" className="form-select-inline" value={mapPositionMode} onChange={e => setMapPositionMode(e.target.value as 'gps' | 'last')}> - <option value="gps">Posición GPS</option> - <option value="last">Donde lo dejé</option> - </select> - </div> - <details className="form-details"> - <summary>¿Qué significa esto?</summary> - <p> - La tabla de horarios puede mostrarse de dos formas: - </p> - <dl> - <dt>Mostrar por orden</dt> - <dd>Las paradas se muestran en el orden en que se visitan. Aplicaciones como Infobus (Vitrasa) usan este estilo.</dd> - <dt>Agrupar por línea</dt> - <dd>Las paradas se agrupan por la línea de autobús. Aplicaciones como iTranvias (A Coruña) o Moovit (más o menos) usan este estilo.</dd> - </dl> - </details> - </section> - <h2>Créditos</h2> - <p> - <a href="https://github.com/arielcostas/urbanovigo-web" className="about-link" rel="nofollow noreferrer noopener"> - Código en GitHub - </a> - - Desarrollado por <a href="https://www.costas.dev" className="about-link" rel="nofollow noreferrer noopener"> - Ariel Costas - </a> - </p> - <p> - Datos obtenidos de <a href="https://datos.vigo.org" className="about-link" rel="nofollow noreferrer noopener">datos.vigo.org</a> bajo - licencia <a href="https://opendefinition.org/licenses/odc-by/" className="about-link" rel="nofollow noreferrer noopener">Open Data Commons Attribution License</a> - </p> - </div> - ) -}
\ No newline at end of file diff --git a/src/pages/StopList.tsx b/src/pages/StopList.tsx deleted file mode 100644 index b965456..0000000 --- a/src/pages/StopList.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from "react"; -import StopDataProvider, { Stop } from "../data/StopDataProvider"; -import StopItem from "../components/StopItem"; -import Fuse from "fuse.js"; - -const placeholders = [ - "Urzaiz", - "Gran Vía", - "Castelao", - "García Barbón", - "Valladares", - "Florida", - "Pizarro", - "Estrada Madrid", - "Sanjurjo Badía" -]; - -export function StopList() { - const [data, setData] = useState<Stop[] | null>(null) - const [searchResults, setSearchResults] = useState<Stop[] | null>(null); - const searchTimeout = useRef<NodeJS.Timeout | null>(null); - - const randomPlaceholder = useMemo(() => placeholders[Math.floor(Math.random() * placeholders.length)], []); - const fuse = useMemo(() => new Fuse(data || [], { threshold: 0.3, keys: ['name.original'] }), [data]); - - useEffect(() => { - StopDataProvider.getStops().then((stops: Stop[]) => setData(stops)) - }, []); - - const handleStopSearch = (event: React.ChangeEvent<HTMLInputElement>) => { - const stopName = event.target.value || ""; - - if (searchTimeout.current) { - clearTimeout(searchTimeout.current); - } - - searchTimeout.current = setTimeout(() => { - if (stopName.length === 0) { - setSearchResults(null); - return; - } - - if (!data) { - console.error("No data available for search"); - return; - } - - const results = fuse.search(stopName); - const items = results.map(result => result.item); - setSearchResults(items); - }, 300); - } - - const favouritedStops = useMemo(() => { - return data?.filter(stop => stop.favourite) ?? [] - }, [data]) - - const recentStops = useMemo(() => { - // no recent items if data not loaded - if (!data) return null; - const recentIds = StopDataProvider.getRecent(); - if (recentIds.length === 0) return null; - // map and filter out missing entries - const stopsList = recentIds - .map(id => data.find(stop => stop.stopId === id)) - .filter((s): s is Stop => Boolean(s)); - return stopsList.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"> - Buscar paradas - </label> - <input className="form-input" type="text" placeholder={randomPlaceholder} 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> - ) -} |
