aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--index.html3
-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
-rw-r--r--staticwebapp.config.json8
6 files changed, 109 insertions, 28 deletions
diff --git a/index.html b/index.html
index abfd384..4812ce5 100644
--- a/index.html
+++ b/index.html
@@ -26,6 +26,9 @@
<link rel="manifest" href="/manifest.webmanifest" />
+ <meta name="robots" content="noindex, nofollow" />
+ <meta name="googlebot" content="noindex, nofollow" />
+
<style>
body {
margin: 0;
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>
diff --git a/staticwebapp.config.json b/staticwebapp.config.json
index d3c815b..5eaaf6c 100644
--- a/staticwebapp.config.json
+++ b/staticwebapp.config.json
@@ -1,5 +1,13 @@
{
"navigationFallback": {
"rewrite": "/index.html"
+ },
+ "globalHeaders": {
+ "X-Content-Type-Options": "nosniff",
+ "X-Frame-Options": "DENY",
+ "X-XSS-Protection": "1; mode=block",
+ "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
+ "Referrer-Policy": "no-referrer",
+ "X-Robots-Tag": "none"
}
}