aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/routes/stoplist.tsx
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-11-03 15:41:26 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-11-03 15:41:26 +0100
commitad53d4dd8c1bad2573a6789087bcdcf58753cd29 (patch)
tree96e4892b356e7c67903739dce6a3f0b90bfc5942 /src/frontend/app/routes/stoplist.tsx
parent769d12a525875d3577b2868208d6315c56ce77d6 (diff)
vibe: Implement user location tracking and sort stops by proximity
Diffstat (limited to 'src/frontend/app/routes/stoplist.tsx')
-rw-r--r--src/frontend/app/routes/stoplist.tsx116
1 files changed, 114 insertions, 2 deletions
diff --git a/src/frontend/app/routes/stoplist.tsx b/src/frontend/app/routes/stoplist.tsx
index 71b7d3c..e77dfb8 100644
--- a/src/frontend/app/routes/stoplist.tsx
+++ b/src/frontend/app/routes/stoplist.tsx
@@ -18,6 +18,10 @@ export default function StopList() {
const [recentIds, setRecentIds] = useState<number[]>([]);
const [favouriteStops, setFavouriteStops] = useState<Stop[]>([]);
const [recentStops, setRecentStops] = useState<Stop[]>([]);
+ const [userLocation, setUserLocation] = useState<{
+ latitude: number;
+ longitude: number;
+ } | null>(null);
const searchTimeout = useRef<NodeJS.Timeout | null>(null);
const randomPlaceholder = useMemo(
@@ -30,6 +34,114 @@ export default function StopList() {
[data],
);
+ const requestUserLocation = useCallback(() => {
+ if (typeof window === "undefined" || !("geolocation" in navigator)) {
+ return;
+ }
+
+ navigator.geolocation.getCurrentPosition(
+ (position) => {
+ setUserLocation({
+ latitude: position.coords.latitude,
+ longitude: position.coords.longitude,
+ });
+ },
+ (error) => {
+ console.warn("Unable to obtain user location", error);
+ },
+ {
+ enableHighAccuracy: false,
+ maximumAge: 5 * 60 * 1000,
+ },
+ );
+ }, []);
+
+ useEffect(() => {
+ if (typeof window === "undefined" || !("geolocation" in navigator)) {
+ return;
+ }
+
+ let permissionStatus: PermissionStatus | null = null;
+
+ const handlePermissionChange = () => {
+ if (permissionStatus?.state === "granted") {
+ requestUserLocation();
+ }
+ };
+
+ const checkPermission = async () => {
+ try {
+ if (navigator.permissions?.query) {
+ permissionStatus = await navigator.permissions.query({ name: "geolocation" });
+ if (permissionStatus.state === "granted") {
+ requestUserLocation();
+ }
+ permissionStatus.addEventListener("change", handlePermissionChange);
+ } else {
+ requestUserLocation();
+ }
+ } catch (error) {
+ console.warn("Geolocation permission check failed", error);
+ requestUserLocation();
+ }
+ };
+
+ checkPermission();
+
+ return () => {
+ permissionStatus?.removeEventListener("change", handlePermissionChange);
+ };
+ }, [requestUserLocation]);
+
+ // Sort stops by proximity when we know where the user is located.
+ const sortedAllStops = useMemo(() => {
+ if (!data) {
+ return [] as Stop[];
+ }
+
+ if (!userLocation) {
+ return [...data].sort((a, b) => a.stopId - b.stopId);
+ }
+
+ const toRadians = (value: number) => (value * Math.PI) / 180;
+ const getDistance = (lat1: number, lon1: number, lat2: number, lon2: number) => {
+ const R = 6371000; // meters
+ const dLat = toRadians(lat2 - lat1);
+ const dLon = toRadians(lon2 - lon1);
+ const a =
+ Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+ Math.cos(toRadians(lat1)) *
+ Math.cos(toRadians(lat2)) *
+ Math.sin(dLon / 2) *
+ Math.sin(dLon / 2);
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ return R * c;
+ };
+
+ return data
+ .map((stop) => {
+ if (typeof stop.latitude !== "number" || typeof stop.longitude !== "number") {
+ return { stop, distance: Number.POSITIVE_INFINITY };
+ }
+
+ const distance = getDistance(
+ userLocation.latitude,
+ userLocation.longitude,
+ stop.latitude,
+ stop.longitude,
+ );
+
+ return { stop, distance };
+ })
+ .sort((a, b) => {
+ if (a.distance === b.distance) {
+ return a.stop.stopId - b.stop.stopId;
+ }
+ return a.distance - b.distance;
+ })
+ .map(({ stop }) => stop);
+ }, [data, userLocation]);
+
// Load favourite and recent IDs immediately from localStorage
useEffect(() => {
setFavouriteIds(StopDataProvider.getFavouriteIds(region));
@@ -184,8 +296,8 @@ export default function StopList() {
</>
)}
{!loading && data
- ?.sort((a, b) => a.stopId - b.stopId)
- .map((stop) => <StopItem key={stop.stopId} stop={stop} />)}
+ ? sortedAllStops.map((stop) => <StopItem key={stop.stopId} stop={stop} />)
+ : null}
</ul>
</div>
</div>