aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-05-12 17:57:33 +0200
committerAriel Costas Guerrero <ariel@costas.dev>2025-05-12 17:57:49 +0200
commit1b54ef6a7da4b2d356a2a33abf98cbb7bf39df2f (patch)
treeec86200071d92ecf7390f472e515cc8a40421f1b /src
parenta327ed48c6d14bf2ccaf8fb0fa4b9ea641481748 (diff)
Fix bugs, add new setting, make app great again
Diffstat (limited to 'src')
-rw-r--r--src/AppContext.tsx50
-rw-r--r--src/components/GroupedTable.tsx2
-rw-r--r--src/pages/Settings.tsx23
-rw-r--r--src/pages/StopList.tsx51
4 files changed, 98 insertions, 28 deletions
diff --git a/src/AppContext.tsx b/src/AppContext.tsx
index a9af208..8b4ffe2 100644
--- a/src/AppContext.tsx
+++ b/src/AppContext.tsx
@@ -4,6 +4,7 @@ import { LatLngTuple } from 'leaflet';
type Theme = 'light' | 'dark';
type TableStyle = 'regular'|'grouped';
+type MapPositionMode = 'gps' | 'last';
interface MapState {
center: LatLngTuple;
@@ -27,6 +28,9 @@ interface AppContextProps {
setUserLocation: (location: LatLngTuple | null) => void;
setLocationPermission: (hasPermission: boolean) => void;
updateMapState: (center: LatLngTuple, zoom: number) => void;
+
+ mapPositionMode: MapPositionMode;
+ setMapPositionMode: (mode: MapPositionMode) => void;
}
// Coordenadas por defecto centradas en Vigo
@@ -74,6 +78,17 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
}, [tableStyle]);
//#endregion
+ //#region Map Position Mode
+ const [mapPositionMode, setMapPositionMode] = useState<MapPositionMode>(() => {
+ const saved = localStorage.getItem('mapPositionMode');
+ return saved === 'last' ? 'last' : 'gps';
+ });
+
+ useEffect(() => {
+ localStorage.setItem('mapPositionMode', mapPositionMode);
+ }, [mapPositionMode]);
+ //#endregion
+
//#region Map State
const [mapState, setMapState] = useState<MapState>(() => {
const savedMapState = localStorage.getItem('mapState');
@@ -98,6 +113,37 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
};
});
+ // Helper: check if coordinates are within Vigo bounds
+ function isWithinVigo([lat, lng]: LatLngTuple): boolean {
+ // Rough bounding box for Vigo
+ return lat >= 42.18 && lat <= 42.30 && lng >= -8.78 && lng <= -8.65;
+ }
+
+ // On app load, if mapPositionMode is 'gps', try to get GPS and set map center
+ useEffect(() => {
+ if (mapPositionMode === 'gps') {
+ if (navigator.geolocation) {
+ navigator.geolocation.getCurrentPosition(
+ (position) => {
+ const { latitude, longitude } = position.coords;
+ const coords: LatLngTuple = [latitude, longitude];
+ if (isWithinVigo(coords)) {
+ setMapState(prev => {
+ const newState = { ...prev, center: coords, zoom: 16, userLocation: coords };
+ localStorage.setItem('mapState', JSON.stringify(newState));
+ return newState;
+ });
+ }
+ },
+ () => {
+ // Ignore error, fallback to last
+ }
+ );
+ }
+ }
+ // If 'last', do nothing (already loaded from localStorage)
+ }, [mapPositionMode]);
+
const setMapCenter = (center: LatLngTuple) => {
setMapState(prev => {
const newState = { ...prev, center };
@@ -170,7 +216,9 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
setMapZoom,
setUserLocation,
setLocationPermission,
- updateMapState
+ updateMapState,
+ mapPositionMode,
+ setMapPositionMode
}}>
{children}
</AppContext.Provider>
diff --git a/src/components/GroupedTable.tsx b/src/components/GroupedTable.tsx
index b4f30a7..58bb5ed 100644
--- a/src/components/GroupedTable.tsx
+++ b/src/components/GroupedTable.tsx
@@ -46,7 +46,7 @@ export const GroupedTable: React.FC<GroupedTable> = ({ data, dataDate }) => {
groupedEstimates[line].map((estimate, idx) => (
<tr key={`${line}-${idx}`}>
{idx === 0 && (
- <td rowSpan={groupedEstimates[line].length} style={{ verticalAlign: 'top' }}>
+ <td rowSpan={groupedEstimates[line].length}>
<LineIcon line={line} />
</td>
)}
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
index e4a1a31..1ad15ab 100644
--- a/src/pages/Settings.tsx
+++ b/src/pages/Settings.tsx
@@ -2,7 +2,7 @@ import { useApp } from "../AppContext";
import "../styles/Settings.css";
export function Settings() {
- const { theme, setTheme, tableStyle, setTableStyle } = useApp();
+ const { theme, setTheme, tableStyle, setTableStyle, mapPositionMode, setMapPositionMode } = useApp();
return (
<div className="about-page">
@@ -15,20 +15,25 @@ export function Settings() {
<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")}
- style={{ backgroundColor: theme === "dark" ? "#333" : "#fff", color: theme === "dark" ? "#fff" : "#000" }}>
+ <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")}
- style={{ backgroundColor: theme === "dark" ? "#333" : "#fff", color: theme === "dark" ? "#fff" : "#000" }}>
+ <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>
@@ -44,16 +49,16 @@ export function Settings() {
</section>
<h2>Créditos</h2>
<p>
- <a href="https://github.com/arielcostas/urbanovigo-web" className="about-link" style={{ color: theme === "dark" ? "#bbb" : "#000" }}>
+ <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" style={{ color: theme === "dark" ? "#bbb" : "#000" }}>
+ 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" style={{ color: theme === "dark" ? "#bbb" : "#000" }}>datos.vigo.org</a> bajo
- licencia <a href="https://opendefinition.org/licenses/odc-by/" style={{ color: theme === "dark" ? "#bbb" : "#000" }}>Open Data Commons Attribution License</a>
+ 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>
)
diff --git a/src/pages/StopList.tsx b/src/pages/StopList.tsx
index a2269ec..b965456 100644
--- a/src/pages/StopList.tsx
+++ b/src/pages/StopList.tsx
@@ -1,35 +1,54 @@
-import { useEffect, useMemo, useState } from "react";
+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"
+ "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 (data) {
- const fuse = new Fuse(data, { keys: ['name'], threshold: 0.3 });
- const results = fuse.search(stopName).map(result => result.item);
- setSearchResults(results);
+ 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(() => {
@@ -50,8 +69,6 @@ export function StopList() {
if (data === null) return <h1 className="page-title">Loading...</h1>
- const randomPlaceholder = placeholders[Math.floor(Math.random() * placeholders.length)];
-
return (
<div className="page-container">
<h1 className="page-title">UrbanoVigo Web</h1>