Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions .github/workflows/build-client.yml

This file was deleted.

24 changes: 24 additions & 0 deletions .github/workflows/eslint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Run Build and Lint Typescript

on:
push:

jobs:
build-and-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

# Run build first to ensure all files exist
- name: Build frontend
run: npm ci && npm run build

# Run ESLint + TypeScript checks
- name: Run lint
run: npm run lint
3 changes: 1 addition & 2 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import About from './pages/About';
import Data from './pages/Data';
import MapKitMap from './components/MapKitMap';
import rawRouteData from './data/routes.json';
import { useState, useEffect, use } from "react";
import WarningBanner from './components/WarningBanner';
import { useState, useEffect } from "react";
import type { ShuttleRouteData } from './ts/types/route';
import Navigation from './components/Navigation';
import config from "./ts/config";
Expand Down
51 changes: 26 additions & 25 deletions client/src/components/MapKitMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import ShuttleIcon from "./ShuttleIcon";
import type { ShuttleRouteData, ShuttleStopData } from "../ts/types/route";
import '../styles/MapKitMap.css';
import type { VehicleInformationMap } from "../ts/types/vehicleLocation";
import type { Route } from "../ts/types/schedule";
import { log } from "../ts/logger";

async function generateRoutePolylines(updatedRouteData: ShuttleRouteData) {
// Use MapKit Directions API to generate polylines for each route segment
Expand Down Expand Up @@ -106,16 +104,15 @@ type MapKitMapProps = {
// @ts-expect-error selectedRoutes is never used
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function MapKitMap({ routeData, vehicles, generateRoutes = false, selectedRoute, setSelectedRoute, isFullscreen = false }: MapKitMapProps) {
const mapRef = useRef(null);
const mapContainerRef = useRef<HTMLDivElement | null>(null);
const mapRef = useRef<mapkit.Map | null>(null);
const [mapLoaded, setMapLoaded] = useState(false);
const token = import.meta.env.VITE_MAPKIT_KEY;
const [map, setMap] = useState<(mapkit.Map | null)>(null);
const token = (import.meta.env.VITE_MAPKIT_KEY || '') as string;
const vehicleOverlays = useRef<Record<string, mapkit.ShuttleAnnotation>>({});


const circleWidth = 15;
const selectedMarkerRef = useRef<mapkit.MarkerAnnotation | null>(null);
const overlays: mapkit.Overlay[] = [];

// source: https://developer.apple.com/documentation/mapkitjs/loading-the-latest-version-of-mapkit-js
const setupMapKitJs = async () => {
Expand All @@ -138,11 +135,11 @@ export default function MapKitMap({ routeData, vehicles, generateRoutes = false,
setMapLoaded(true);
};
mapkitScript();
}, []);
}, [token]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since token never changes, we shouldnt useMemo it. In react 19.2, there is an useEventEffect function that seems to be perfect for this. In addition, we don't need to fix all the dependency array issues since they are warnings and not errors.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will change. I pass --max-warnings=0, so it doesn't tolerate warnings.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eslint github actions doesnt fail on warnings i thought?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think by default it doesn't, but if you pass --max-warnings it will.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to enforce 0 warnings? I feel like sometimes the warnings are fine as react dependency arrays don't really need some of the dependencies eslint is hinting at


// create the map
useEffect(() => {
if (mapLoaded) {
if (mapContainerRef.current && mapLoaded) {

// center on RPI
const center = new mapkit.Coordinate(42.730216, -73.675690);
Expand All @@ -161,7 +158,7 @@ export default function MapKitMap({ routeData, vehicles, generateRoutes = false,
};

// create the map
const thisMap = new mapkit.Map(mapRef.current!, mapOptions);
const thisMap = new mapkit.Map(mapContainerRef.current, mapOptions);
// set zoom and boundary limits
thisMap.setCameraZoomRangeAnimated(
new mapkit.CameraZoomRange(200, 3000),
Expand Down Expand Up @@ -289,22 +286,23 @@ export default function MapKitMap({ routeData, vehicles, generateRoutes = false,
// thisMap.element.removeEventListener('mousemove', _);
};

setMap(thisMap);
mapRef.current = thisMap;
}

// Cleanup on component unmount
return () => {
if (map && map._hoverCleanup) {
map._hoverCleanup();
if (mapRef.current && mapRef.current._hoverCleanup) {
mapRef.current._hoverCleanup();
}
};
}, [mapLoaded]);
}, [mapLoaded, setSelectedRoute]);

// add fixed details to the map
// includes routes and stops
useEffect(() => {
if (!map || !routeData) return;
if (!mapLoaded || !mapRef.current || !routeData) return;

const overlays: mapkit.Overlay[] = [];

// display stop overlays
for (const [route, thisRouteData] of Object.entries(routeData)) {
Expand Down Expand Up @@ -337,7 +335,7 @@ export default function MapKitMap({ routeData, vehicles, generateRoutes = false,

function displayRouteOverlays(routeData: ShuttleRouteData) {
// display route overlays
for (const [_route, thisRouteData] of Object.entries(routeData)) {
for (const [_, thisRouteData] of Object.entries(routeData)) {
// for route (WEST, NORTH)
const routePolylines = thisRouteData.ROUTES?.map(
// for segment (STOP1 -> STOP2, STOP2 -> STOP3, ...)
Expand All @@ -360,22 +358,25 @@ export default function MapKitMap({ routeData, vehicles, generateRoutes = false,

if (generateRoutes) {
// generate polylines for routes
const routeDataCopy = JSON.parse(JSON.stringify(routeData)); // deep copy to avoid mutating original
const routeDataCopy = JSON.parse(JSON.stringify(routeData)) as ShuttleRouteData;
generateRoutePolylines(routeDataCopy).then((updatedRouteData) => {
displayRouteOverlays(updatedRouteData);
map.addOverlays(overlays);
if (mapRef.current) {
mapRef.current.addOverlays(overlays);
}
});
} else {
// use pre-generated polylines
displayRouteOverlays(routeData);
map.addOverlays(overlays);
mapRef.current.addOverlays(overlays);
}

}, [map, routeData]);
}, [routeData, generateRoutes, mapLoaded]);

// display vehicles on map
useEffect(() => {
if (!map || !vehicles) return;
if (!mapRef.current || !vehicles) return;
const map = mapRef.current;

Object.keys(vehicles).forEach((key) => {
const vehicle = vehicles[key];
Expand All @@ -391,7 +392,7 @@ export default function MapKitMap({ routeData, vehicles, generateRoutes = false,
const routeKey = vehicle.route_name as keyof typeof routeData;
const info = routeData[routeKey] as { COLOR?: string };
return info.COLOR ?? "#444444";

})();

// Render ShuttleIcon JSX to a static SVG string
Expand All @@ -405,7 +406,7 @@ export default function MapKitMap({ routeData, vehicles, generateRoutes = false,
existingAnnotation.subtitle = `${vehicle.speed_mph.toFixed(1)} mph`;

// Handle route status updates
// If shuttle does not have a route null
// If shuttle does not have a route null
if (vehicle.route_name === null) {
// shuttle off-route (exiting)
if (existingAnnotation.lockedRoute) {
Expand Down Expand Up @@ -448,14 +449,14 @@ export default function MapKitMap({ routeData, vehicles, generateRoutes = false,
delete vehicleOverlays.current[key];
}
});
}, [map, vehicles, routeData]);
}, [vehicles, routeData]);



return (
<div
className={isFullscreen ? 'map-fullscreen' : 'map'}
ref={mapRef}
ref={mapContainerRef}
>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default function Navigation({ GIT_REV }: { GIT_REV: string }) {
<div className='big'>
<div className='big-footer'>
<div className='git-copy'>
<a href='https://github.com/wtg/shubble' target='_blank'>
<a href='https://github.com/wtg/shubble' target='_blank' rel="noreferrer">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#000000" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2" /></svg>
</a>
<p>&copy; 2025 SHUBBLE, an RCOS Project</p>
Expand Down
20 changes: 8 additions & 12 deletions client/src/components/Schedule.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo } from 'react';
import '../styles/Schedule.css';
import rawRouteData from '../data/routes.json';
import rawAggregatedSchedule from '../data/aggregated_schedule.json';
Expand All @@ -20,18 +20,20 @@ export default function Schedule({ selectedRoute, setSelectedRoute }: SchedulePr
throw new Error('setSelectedRoute must be a function');
}

const now = new Date();
const now = useMemo(() => new Date(), []);
const [selectedDay, setSelectedDay] = useState(now.getDay());
const [routeNames, setRouteNames] = useState(Object.keys(aggregatedSchedule[selectedDay]));
const [stopNames, setStopNames] = useState<string[]>([]);
const [schedule, setSchedule] = useState<AggregatedDaySchedule>(aggregatedSchedule[selectedDay]);

// Define safe values to avoid repeated null checks
const safeSelectedRoute = selectedRoute || routeNames[0];

// Update schedule and routeNames when selectedDay changes
useEffect(() => {
setSchedule(aggregatedSchedule[selectedDay]);
}, [selectedDay]);

// Update schedule and routeNames when selectedDay changes
useEffect(() => {
setRouteNames(Object.keys(aggregatedSchedule[selectedDay]));
// If parent hasn't provided a selectedRoute yet, pick the first available one
const firstRoute = Object.keys(aggregatedSchedule[selectedDay])[0];
Expand All @@ -40,12 +42,6 @@ export default function Schedule({ selectedRoute, setSelectedRoute }: SchedulePr
}
}, [selectedDay, selectedRoute, setSelectedRoute]);

// Update stopNames when selectedRoute changes
useEffect(() => {
if (!safeSelectedRoute || !(safeSelectedRoute in routeData)) return;
setStopNames(routeData[safeSelectedRoute as keyof typeof routeData].STOPS);
}, [selectedRoute]);

// Handle day change from dropdown
const handleDayChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedDay(parseInt(e.target.value));
Expand All @@ -54,7 +50,7 @@ export default function Schedule({ selectedRoute, setSelectedRoute }: SchedulePr
const timeToDate = (timeStr: string): Date => {
const [time, modifier] = timeStr.trim().split(" ");

// eslint-disable-next-line prefer-const

let [hours, minutes] = time.split(":").map(Number);
if (modifier.toUpperCase() === "PM" && hours !== 12) {
hours += 12;
Expand Down Expand Up @@ -94,7 +90,7 @@ export default function Schedule({ selectedRoute, setSelectedRoute }: SchedulePr
if (currentTimeRow) {
currentTimeRow.scrollIntoView({ behavior: "auto" });
}
}, [selectedRoute, selectedDay, schedule]);
}, [selectedRoute, selectedDay, schedule, now]);


const daysOfTheWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
Expand Down
4 changes: 4 additions & 0 deletions client/src/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ declare global {
_hoverCleanup?: () => void;
}
const loadedLibraries: string[];
interface ShuttleAnnotation extends ImageAnnotation {
lockedRoute?: string | null;
url: Record<number, string>;
}
}
interface Window {
initMapKit?: (value?: unknown) => void;
Expand Down
6 changes: 3 additions & 3 deletions client/src/pages/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function About() {
return (prevIndex + 1) % words.length;
});
}, 2000);
}, []);
}, [words.length]);

return (
<div className='about'>
Expand All @@ -32,10 +32,10 @@ export default function About() {
Shubble is an open source project under the Rensselaer Center for Open Source (RCOS).
</p>
<p>
Have an idea to improve it? Contributions are welcome. Visit our <a href='https://github.com/wtg/shubble' target='_blank'>Github Repository</a> to learn more.
Have an idea to improve it? Contributions are welcome. Visit our <a href='https://github.com/wtg/shubble' target='_blank' rel="noreferrer">Github Repository</a> to learn more.
</p>
<p>
Interested in Shubble's data? Take a look at our
Interested in Shubble &apos s data? Take a look at our
<Link to='/data'>
<span className = 'link1'>data page</span>
</Link>.
Expand Down
4 changes: 2 additions & 2 deletions client/src/pages/Data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function Data() {
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
const data = await response.json() as VehicleInformationMap;
setShuttleData(data);
} catch (error) {
console.error('Error fetching shuttleData:', error);
Expand All @@ -35,7 +35,7 @@ export default function Data() {
setSelectedShuttleID(Object.keys(shuttleData)[0]);
}
}
}, [shuttleData]);
}, [shuttleData, selectedShuttleID]);

return (
<>
Expand Down
8 changes: 5 additions & 3 deletions client/src/pages/LiveLocation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import routeData from '../data/routes.json';
import type { VehicleInformationMap } from '../ts/types/vehicleLocation';
import type { ShuttleRouteData } from '../ts/types/route';
import aggregatedSchedule from '../data/aggregated_schedule.json';
import type { AggregatedScheduleType } from '../ts/types/schedule';
const typedAggregatedSchedule = aggregatedSchedule as AggregatedScheduleType;

export default function LiveLocation() {

Expand All @@ -21,10 +23,10 @@ export default function LiveLocation() {

// Filter routeData to only include routes present in aggregatedSchedule
useEffect(() => {
// TODO: figure out how to make this type correct...
// TODO: figure out how to clean up this type...
setFilteredRouteData(
Object.fromEntries(
Object.entries(routeData).filter(([routeName]) => aggregatedSchedule.some(daySchedule => routeName in daySchedule))
Object.entries(routeData).filter(([routeName]) => typedAggregatedSchedule.some(daySchedule => routeName in daySchedule))
) as unknown as ShuttleRouteData
);
}, []);
Expand All @@ -37,7 +39,7 @@ export default function LiveLocation() {
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
const data = await response.json() as VehicleInformationMap;
setLocation(data);
} catch (error) {
console.error('Error fetching location:', error);
Expand Down
Loading