diff --git a/README.md b/README.md index 8968710..ea8ba91 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,26 @@ This project gives you a docker image that you can run locally to pull MTA data and self-host a webpage that looks very similar to the classic MTA Signs found in the metro. +![ui-example.png](attachments/ui-example.png) + Initially designed to run directly on a raspberry pi, it has been containerized and now runs anywhere you can install podman or docker! +Additionally, this can be run without the frontend in case you just wanted a slightly more sane way to query MTA's data +sources compared to their stock APIs. + +## Features + +- **Real-time MTA Data**: Live train arrival times for NYC subway + stations - [available for free here](https://api.mta.info/#/subwayRealTimeFeeds) +- **Configurable Stations**: Monitor multiple stations simultaneously with separate cards +- **Line Filtering**: Show/hide specific transit lines for each station +- **Direction Selection**: Toggle between North/South bound trains independently +- **Responsive Design**: Works on desktop, tablet, and mobile displays +- **Configuration Persistence**: Save your selected stations and preferences to localStorage or export/import them via + JSON +- **Docker Ready**: Easy deployment with Docker or Docker Compose + ## Running the Docker Image ### Prerequisites diff --git a/attachments/ui-example.png b/attachments/ui-example.png new file mode 100644 index 0000000..b935762 Binary files /dev/null and b/attachments/ui-example.png differ diff --git a/mta-sign-ui/components/StationSelector.tsx b/mta-sign-ui/components/StationSelector.tsx index 71820ef..a0f45b5 100644 --- a/mta-sign-ui/components/StationSelector.tsx +++ b/mta-sign-ui/components/StationSelector.tsx @@ -59,12 +59,12 @@ export default function StationSelector({ selectedStation, onSelect }: StationSe return (
setIsOpen(!isOpen)} > { setSearch(e.target.value) @@ -74,7 +74,7 @@ export default function StationSelector({ selectedStation, onSelect }: StationSe e.stopPropagation() setIsOpen(true) }} - className="flex-1 bg-transparent text-white placeholder-gray-400 outline-none text-sm md:text-base lg:text-lg" + className="flex-1 bg-transparent text-white placeholder-gray-400 outline-none text-sm md:text-base" /> { +const DirectionCard = ({ direction, routes, isEndOfLine = false }: DirectionCardProps) => { return (
-
-

- {direction} -

-
+ {!isEndOfLine && ( +
+

+ {direction} +

+
+ )}
{routes && routes.length > 0 ? ( routes.map((route, index) => ( diff --git a/mta-sign-ui/components/trains/StationCard.tsx b/mta-sign-ui/components/trains/StationCard.tsx index 7588dd6..e176a84 100644 --- a/mta-sign-ui/components/trains/StationCard.tsx +++ b/mta-sign-ui/components/trains/StationCard.tsx @@ -57,20 +57,35 @@ export default function StationCard({ configId, initialConfig, availableLines, o const fetchStationData = useCallback(async () => { if (!selectedStation) return - try { - if (showNorth) { + if (showNorth) { + try { const response = await axios.post(`/api/mta/${selectedStation.id}N`) setNorthData(response.data) + } catch (err) { + console.error('Error fetching north station data:', err) + setNorthData(null) } - if (showSouth) { + } + + if (showSouth) { + try { const response = await axios.post(`/api/mta/${selectedStation.id}S`) setSouthData(response.data) + } catch (err) { + console.error('Error fetching south station data:', err) + setSouthData(null) } - } catch (err) { - console.error('Error fetching station data:', err) } }, [selectedStation, showNorth, showSouth]) + // Clear old data when station changes to prevent showing stale data from previous station + useEffect(() => { + if (selectedStation) { + setNorthData(null) + setSouthData(null) + } + }, [selectedStation?.id]) + useEffect(() => { if (selectedStation) { fetchStationData() @@ -149,6 +164,7 @@ export default function StationCard({ configId, initialConfig, availableLines, o showSouth={showSouth} northRoutes={northData ? filterRoutes(northData.routes || []) : []} southRoutes={southData ? filterRoutes(southData.routes || []) : []} + availableDirections={availableDirections} />
) @@ -179,14 +195,37 @@ function Header({ onToggleLine, onRemove, }: HeaderProps) { - return ( -
-
- -
+ const [showSearch, setShowSearch] = useState(false) -
- {availableDirections.has('North') && ( + return ( +
+ {/* Station Name Title and Search Row */} +
+ {selectedStation && ( +

+ {selectedStation.name} +

+ )} + + {/* Search Button */} + {showSearch ? ( +
+ { + onSelectStation(station) + setShowSearch(false) + }} /> +
+ ) : ( + + )} + +
+ {availableDirections.has('North') && (
) @@ -244,9 +284,10 @@ interface ContentProps { showSouth: boolean northRoutes: Route[] southRoutes: Route[] + availableDirections: Set } -function Content({ selectedStation, showNorth, showSouth, northRoutes, southRoutes }: ContentProps) { +function Content({ selectedStation, showNorth, showSouth, northRoutes, southRoutes, availableDirections }: ContentProps) { if (!selectedStation) { return (
@@ -255,16 +296,23 @@ function Content({ selectedStation, showNorth, showSouth, northRoutes, southRout ) } + // Determine if this is an end-of-line station (only one direction available) + const isEndOfLine = availableDirections.size === 1 + + // Only render directions that are both available AND selected + const renderNorth = showNorth && availableDirections.has('North') + const renderSouth = showSouth && availableDirections.has('South') + return (
- {showNorth && ( -
- + {renderNorth && ( +
+
)} - {showSouth && ( + {renderSouth && (
- +
)}