aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-06-24 16:02:02 +0200
committerAriel Costas Guerrero <ariel@costas.dev>2025-06-24 16:02:02 +0200
commitf65b4e1e0d5648038823962349279be4badc68ed (patch)
tree402635814103fde9060c8710523bb4b11ba0a01d
parentdc4a7f316c1e3f3392ffd68b6a432eddd7013868 (diff)
Refactor navigation structure: move NavBar to its own component, implement geolocation handling, and remove unused isWithinVigo function from AppContext.
-rw-r--r--package-lock.json92
-rw-r--r--src/frontend/app/AppContext.tsx40
-rw-r--r--src/frontend/app/components/NavBar.tsx84
-rw-r--r--src/frontend/app/root.tsx54
4 files changed, 124 insertions, 146 deletions
diff --git a/package-lock.json b/package-lock.json
index 18a459f..07400f9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,8 +13,7 @@
],
"devDependencies": {
"concurrently": "^9.1.2",
- "prettier": "^3.5.3",
- "vite-tsconfig-paths": "^5.1.4"
+ "prettier": "^3.5.3"
}
},
"node_modules/@ampproject/remapping": {
@@ -523,7 +522,6 @@
"os": [
"aix"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -541,7 +539,6 @@
"os": [
"android"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -559,7 +556,6 @@
"os": [
"android"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -577,7 +573,6 @@
"os": [
"android"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -595,7 +590,6 @@
"os": [
"darwin"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -613,7 +607,6 @@
"os": [
"darwin"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -631,7 +624,6 @@
"os": [
"freebsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -649,7 +641,6 @@
"os": [
"freebsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -667,7 +658,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -685,7 +675,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -703,7 +692,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -721,7 +709,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -739,7 +726,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -757,7 +743,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -775,7 +760,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -793,7 +777,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -811,7 +794,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -829,7 +811,6 @@
"os": [
"netbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -847,7 +828,6 @@
"os": [
"netbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -865,7 +845,6 @@
"os": [
"openbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -883,7 +862,6 @@
"os": [
"openbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -901,7 +879,6 @@
"os": [
"sunos"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -919,7 +896,6 @@
"os": [
"win32"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -937,7 +913,6 @@
"os": [
"win32"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -955,7 +930,6 @@
"os": [
"win32"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1752,8 +1726,7 @@
"optional": true,
"os": [
"android"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.41.1",
@@ -1767,8 +1740,7 @@
"optional": true,
"os": [
"android"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.41.1",
@@ -1782,8 +1754,7 @@
"optional": true,
"os": [
"darwin"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.41.1",
@@ -1797,8 +1768,7 @@
"optional": true,
"os": [
"darwin"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.41.1",
@@ -1812,8 +1782,7 @@
"optional": true,
"os": [
"freebsd"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.41.1",
@@ -1827,8 +1796,7 @@
"optional": true,
"os": [
"freebsd"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.41.1",
@@ -1842,8 +1810,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.41.1",
@@ -1857,8 +1824,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.41.1",
@@ -1872,8 +1838,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.41.1",
@@ -1887,8 +1852,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.41.1",
@@ -1902,8 +1866,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.41.1",
@@ -1917,8 +1880,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.41.1",
@@ -1932,8 +1894,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.41.1",
@@ -1947,8 +1908,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.41.1",
@@ -1962,8 +1922,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.41.1",
@@ -1990,8 +1949,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.41.1",
@@ -2005,8 +1963,7 @@
"optional": true,
"os": [
"win32"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.41.1",
@@ -2020,8 +1977,7 @@
"optional": true,
"os": [
"win32"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.41.1",
@@ -2035,8 +1991,7 @@
"optional": true,
"os": [
"win32"
- ],
- "peer": true
+ ]
},
"node_modules/@types/estree": {
"version": "1.0.7",
@@ -3798,7 +3753,6 @@
"os": [
"darwin"
],
- "peer": true,
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
@@ -6830,10 +6784,8 @@
"react-map-gl": "^8.0.4",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.0",
- "vite": "^6.3.5"
- },
- "engines": {
- "node": ">=20.0.0"
+ "vite": "^6.3.5",
+ "vite-tsconfig-paths": "5.1.4"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "*"
diff --git a/src/frontend/app/AppContext.tsx b/src/frontend/app/AppContext.tsx
index 7ca85bd..d8db66d 100644
--- a/src/frontend/app/AppContext.tsx
+++ b/src/frontend/app/AppContext.tsx
@@ -113,46 +113,6 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
};
});
- // Helper: check if coordinates are within Vigo bounds
- function isWithinVigo(lngLat: LngLatLike): boolean {
- let lng: number, lat: number;
- if (Array.isArray(lngLat)) {
- [lng, lat] = lngLat;
- } else if ('lng' in lngLat && 'lat' in lngLat) {
- lng = lngLat.lng;
- lat = lngLat.lat;
- } else {
- return false;
- }
- // 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: LngLatLike = [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: LngLatLike) => {
setMapState(prev => {
const newState = { ...prev, center };
diff --git a/src/frontend/app/components/NavBar.tsx b/src/frontend/app/components/NavBar.tsx
new file mode 100644
index 0000000..091cc21
--- /dev/null
+++ b/src/frontend/app/components/NavBar.tsx
@@ -0,0 +1,84 @@
+import { Link } from "react-router";
+import { Map, MapPin, Settings } from "lucide-react";
+import { useApp } from "../AppContext";
+import type { LngLatLike } from "maplibre-gl";
+
+// Helper: check if coordinates are within Vigo bounds
+function isWithinVigo(lngLat: LngLatLike): boolean {
+ let lng: number, lat: number;
+ if (Array.isArray(lngLat)) {
+ [lng, lat] = lngLat;
+ } else if ('lng' in lngLat && 'lat' in lngLat) {
+ lng = lngLat.lng;
+ lat = lngLat.lat;
+ } else {
+ return false;
+ }
+ // Rough bounding box for Vigo
+ return lat >= 42.18 && lat <= 42.30 && lng >= -8.78 && lng <= -8.65;
+}
+
+export default function NavBar() {
+ const { mapState, updateMapState, mapPositionMode } = useApp();
+
+ const navItems = [
+ {
+ name: 'Paradas',
+ icon: MapPin,
+ path: '/stops'
+ },
+ {
+ name: 'Mapa',
+ icon: Map,
+ path: '/map',
+ callback: () => {
+ if (mapPositionMode !== 'gps') {
+ return;
+ }
+
+ if (!('geolocation' in navigator)) {
+ return;
+ }
+
+ navigator.geolocation.getCurrentPosition(
+ (position) => {
+ const { latitude, longitude } = position.coords;
+ const coords: LngLatLike = [latitude, longitude];
+ if (isWithinVigo(coords)) {
+ updateMapState(coords, 16);
+ }
+ },
+ () => { }
+ );
+ }
+ },
+ {
+ name: 'Ajustes',
+ icon: Settings,
+ path: '/settings'
+ }
+ ];
+
+ return (
+ <nav className="navigation-bar">
+ {navItems.map(item => {
+ const Icon = item.icon;
+ const isActive = location.pathname.startsWith(item.path);
+
+ return (
+ <Link
+ key={item.name}
+ to={item.path}
+ className={`navigation-bar__link ${isActive ? 'active' : ''}`}
+ onClick={item.callback ? item.callback : undefined}
+ title={item.name}
+ aria-label={item.name}
+ >
+ <Icon size={24} />
+ <span>{item.name}</span>
+ </Link>
+ );
+ })}
+ </nav>
+ );
+}
diff --git a/src/frontend/app/root.tsx b/src/frontend/app/root.tsx
index d90dba0..9d59ce7 100644
--- a/src/frontend/app/root.tsx
+++ b/src/frontend/app/root.tsx
@@ -16,9 +16,8 @@ import "./root.css";
import "maplibre-theme/icons.default.css";
import "maplibre-theme/modern.css";
import { Protocol } from "pmtiles";
-import maplibregl from "maplibre-gl";
+import maplibregl, { type LngLatLike } from "maplibre-gl";
import { AppProvider } from "./AppContext";
-import { Map, MapPin, Settings } from "lucide-react";
const pmtiles = new Protocol();
maplibregl.addProtocol("pmtiles", pmtiles.tile);
//#endregion
@@ -81,48 +80,31 @@ export function Layout({ children }: { children: React.ReactNode }) {
);
}
-export default function App() {
- const navItems = [
- {
- name: 'Paradas',
- icon: MapPin,
- path: '/stops'
- },
- {
- name: 'Mapa',
- icon: Map,
- path: '/map'
- },
- {
- name: 'Ajustes',
- icon: Settings,
- path: '/settings'
+ // Helper: check if coordinates are within Vigo bounds
+ function isWithinVigo(lngLat: LngLatLike): boolean {
+ let lng: number, lat: number;
+ if (Array.isArray(lngLat)) {
+ [lng, lat] = lngLat;
+ } else if ('lng' in lngLat && 'lat' in lngLat) {
+ lng = lngLat.lng;
+ lat = lngLat.lat;
+ } else {
+ return false;
}
- ];
+ // Rough bounding box for Vigo
+ return lat >= 42.18 && lat <= 42.30 && lng >= -8.78 && lng <= -8.65;
+ }
+
+import NavBar from "./components/NavBar";
+export default function App() {
return (
<AppProvider>
<main className="main-content">
<Outlet />
</main>
<footer>
- <nav className="navigation-bar">
- {navItems.map(item => {
- const Icon = item.icon;
- const isActive = location.pathname.startsWith(item.path);
-
- return (
- <Link
- key={item.name}
- to={item.path}
- className={`navigation-bar__link ${isActive ? 'active' : ''}`}
- >
- <Icon size={24} />
- <span>{item.name}</span>
- </Link>
- );
- })}
- </nav>
+ <NavBar />
</footer>
</AppProvider>