diff --git a/app/components/map/StopCallout.tsx b/app/components/map/StopCallout.tsx index 4004b80..3d685a2 100644 --- a/app/components/map/StopCallout.tsx +++ b/app/components/map/StopCallout.tsx @@ -1,49 +1,85 @@ -import React, { memo, useState, useCallback } from 'react' -import { View, Text, LayoutChangeEvent } from 'react-native' +import React, { memo } from 'react' +import { View, Text, ActivityIndicator, TouchableOpacity } from 'react-native' import { Callout } from 'react-native-maps' import BusIcon from '../ui/BusIcon' import { IDirection, IMapRoute, IStop } from '../../../utils/interfaces' -import AmenityRow from "../ui/AmenityRow"; import { useStopEstimate } from 'app/data/api_query' +import moment from 'moment' +import CalloutTimeBubble from '../ui/CalloutTimeBubble' +import { lightMode } from 'app/theme' +import AmenityRow from '../ui/AmenityRow' +import useAppStore from 'app/data/app_state' interface Props { - stop: IStop - tintColor: string - route: IMapRoute - direction: IDirection + stop: IStop + tintColor: string + route: IMapRoute + direction: IDirection } -// Stop callout with amentities +// Stop callout with time bubbles const StopCallout: React.FC = ({ stop, tintColor, route, direction }) => { - // Calculate size of callout based on the contentSize - const [contentSize, setContentSize] = useState([100, 15]); + const scrollToStop = useAppStore(state => state.scrollToStop); - const { data: estimate } = useStopEstimate(route.key, direction.key, stop.stopCode); - - const handleLayout = useCallback((event: LayoutChangeEvent) => { - const { width, height } = event.nativeEvent.layout; - - setContentSize([width, height]); - }, [setContentSize]); + const { data: estimate, isLoading } = useStopEstimate(route.key, direction.key, stop.stopCode); return ( - - - + + { scrollToStop(stop) }} + > - {stop.name} - + {stop.name} + + - + { estimate?.routeDirectionTimes[0]?.nextDeparts.length !== 0 ? + + + { estimate?.routeDirectionTimes[0]?.nextDeparts.map((departureTime, index) => { + const date = moment(departureTime.estimatedDepartTimeUtc ?? departureTime.scheduledDepartTimeUtc ?? ""); + const relative = date.diff(moment(), "minutes"); + return ( + + ) + })} + + + : ( isLoading ? + + : + No upcoming departures + ) + } ) diff --git a/app/components/sheets/RouteDetails.tsx b/app/components/sheets/RouteDetails.tsx index b1a4a1b..fa82249 100644 --- a/app/components/sheets/RouteDetails.tsx +++ b/app/components/sheets/RouteDetails.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import { View, Text, TouchableOpacity, NativeSyntheticEvent } from "react-native"; -import { BottomSheetModal, BottomSheetView, BottomSheetFlatList } from "@gorhom/bottom-sheet"; +import { BottomSheetModal, BottomSheetView, BottomSheetFlatList, BottomSheetFlatListMethods } from "@gorhom/bottom-sheet"; import SegmentedControl, { NativeSegmentedControlIOSChangeEvent } from "@react-native-segmented-control/segmented-control"; import { Ionicons } from '@expo/vector-icons'; import { IMapRoute, IPatternPath, IStop } from "../../../utils/interfaces"; @@ -18,13 +18,19 @@ interface SheetProps { // Display details when a route is selected const RouteDetails: React.FC = ({ sheetRef }) => { + + const flatListRef = React.useRef(null); + const currentSelectedRoute = useAppStore((state) => state.selectedRoute); const clearSelectedRoute = useAppStore((state) => state.clearSelectedRoute); + const [futurePosition, setFuturePosition] = useState(-1); + const setSelectedRouteDirection = useAppStore(state => state.setSelectedRouteDirection); const setSelectedStop = useAppStore(state => state.setSelectedStop); const setPoppedUpStopCallout = useAppStore(state => state.setPoppedUpStopCallout); const selectedRouteDirection = useAppStore(state => state.selectedRouteDirection); + const setScrollToStop = useAppStore(state => state.setScrollToStop); const theme = useAppStore(state => state.theme); const { data: stopEstimates } = useStopEstimate( @@ -33,6 +39,13 @@ const RouteDetails: React.FC = ({ sheetRef }) => { currentSelectedRoute?.patternPaths[0]?.patternPoints[0]?.stop?.stopCode ?? "" ) + setScrollToStop((stop) => { + sheetRef.current?.snapToIndex(2); + const index = processedStops.findIndex(st => st.stopCode === stop.stopCode); + + setFuturePosition(index); + }) + // Controls SegmentedControl const [selectedDirectionIndex, setSelectedDirectionIndex] = useState(0); @@ -121,6 +134,12 @@ const RouteDetails: React.FC = ({ sheetRef }) => { enablePanDownToClose={false} backgroundStyle={{ backgroundColor: theme.background }} handleIndicatorStyle={{backgroundColor: theme.divider}} + onChange={() => { + if (futurePosition !== -1) { + flatListRef.current?.scrollToIndex({ index: futurePosition, animated: true }); + setFuturePosition(-1); + } + }} > {selectedRoute && @@ -158,6 +177,7 @@ const RouteDetails: React.FC = ({ sheetRef }) => { { selectedRoute && = ({time, color, textColor, live}) => { + return ( + + + {time} + + + { live && + + } + + ) +} + +export default CalloutTimeBubble; \ No newline at end of file diff --git a/app/components/ui/StopCell.tsx b/app/components/ui/StopCell.tsx index 6badeed..8480210 100644 --- a/app/components/ui/StopCell.tsx +++ b/app/components/ui/StopCell.tsx @@ -34,7 +34,7 @@ const StopCell: React.FC = ({ stop, route, direction, color, disabled, se const estimate = stopEstimate.routeDirectionTimes[0]!; - let totalDeviation = 0; + let deviation = 0; for (const departTime of estimate.nextDeparts) { const estimatedTime = moment(departTime.estimatedDepartTimeUtc ?? ""); @@ -43,23 +43,23 @@ const StopCell: React.FC = ({ stop, route, direction, color, disabled, se const delayLength = estimatedTime.diff(scheduledTime, "seconds"); if (!isNaN(delayLength)) { - totalDeviation += delayLength; + deviation = delayLength; + break; } } - const avgDeviation = totalDeviation / estimate.nextDeparts.length / (60); - const roundedDeviation = Math.round(avgDeviation); + const roundedDeviation = Math.round(deviation / 60); if (estimate.directionKey === "") { setStatus('Loading'); } else if (estimate.nextDeparts.length === 0) { - setStatus("No times to show"); + setStatus("No upcoming departures"); } else if (roundedDeviation > 0) { setStatus(`${roundedDeviation} ${roundedDeviation > 1 ? "minutes" : "minute"} late`); } else if (roundedDeviation < 0) { setStatus(`${Math.abs(roundedDeviation)} ${Math.abs(roundedDeviation) > 1 ? "minutes" : "minute"} early`); } else { - setStatus('On Time'); + setStatus('On time'); } }, [stopEstimate]); diff --git a/app/data/app_state.ts b/app/data/app_state.ts index 0fa2023..216fb8a 100644 --- a/app/data/app_state.ts +++ b/app/data/app_state.ts @@ -22,7 +22,7 @@ interface AppState { selectedStop: IStop | null, setSelectedStop: (selectedStop: IStop | null) => void, - // TODO: Switch to Provider Functions + // TODO: Switch to ContextProvider Functions presentSheet: (sheet: "routeDetails" | "alerts" | "stopTimetable" | "settings" | "alertsDetail") => void setPresentSheet: (presentSheet: (sheet: "routeDetails" | "alerts" | "stopTimetable" | "settings" | "alertsDetail") => void) => void @@ -32,12 +32,15 @@ interface AppState { selectedTimetableDate: Date | null, setSelectedTimetableDate: (selectedTimetableDate: Date | null) => void - // TODO: Switch to Provider Functions + // TODO: Switch to Context Provider Functions zoomToStopLatLng: (lat: number, lng: number) => void setZoomToStopLatLng: (zoomToStopLatLng: (lat: number, lng: number) => void) => void poppedUpStopCallout: IStop | null, setPoppedUpStopCallout: (poppedUpStopCallout: IStop | null) => void + + scrollToStop: (stop: IStop) => void + setScrollToStop: (scrollToStop: (stop: IStop) => void) => void } const useAppStore = create()((set) => ({ @@ -74,7 +77,10 @@ const useAppStore = create()((set) => ({ setZoomToStopLatLng: (zoomToStopLatLng) => set(() => ({ zoomToStopLatLng })), poppedUpStopCallout: null, - setPoppedUpStopCallout: (poppedUpStopCallout) => set(() => ({ poppedUpStopCallout })) + setPoppedUpStopCallout: (poppedUpStopCallout) => set(() => ({ poppedUpStopCallout })), + + scrollToStop: (stop) => {console.log(stop)}, + setScrollToStop: (scrollToStop) => set(() => ({ scrollToStop: scrollToStop })) })); export default useAppStore; \ No newline at end of file