diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a17846c4..c55576e8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,15 +1,19 @@ ## Describe your changes + (provide a description of changes and a overview of solution) ## Issue number and link + (if this PR is in response to a issue, put the link to the issue here) ## Dependency Changes + (place any new libraries or updates to exisiting libraries here, you can check by looking at the changes to `package.json`) ## Checklist before requesting a review + - [ ] I have performed a self-review of my code - [ ] I have verified that any UI changes I have made work in dark mode - [ ] All of my GitHub checks have passed - [ ] I have added any new packages/updates above -- [ ] I have verified that I am not submitting other changes/features outside the scope of my PR \ No newline at end of file +- [ ] I have verified that I am not submitting other changes/features outside the scope of my PR diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 452d1ecb..dc030261 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: "Build App and Upload" +name: 'Build App and Upload' on: # manual trigger but change to any supported event @@ -11,14 +11,13 @@ on: default: 'all' type: choice options: - - all - - ios - - android + - all + - ios + - android release: types: [published] - jobs: build_ios: runs-on: macos-latest @@ -37,22 +36,22 @@ jobs: run: /usr/bin/xcodebuild -version - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v4 - name: Install Fastlane and Fix CocoaPods run: | sudo gem install cocoapods -v 1.15.2 sudo gem install fastlane -NV fastlane --version - + - name: Install Node.js uses: actions/setup-node@v4 with: node-version: '22.x' - + - name: Install Dependencies - run: npm install - + run: pnpm install + - name: Expo Prebuild [iOS] run: | npx expo prebuild --platform ios --npm @@ -69,7 +68,7 @@ jobs: build_android: runs-on: ubuntu-latest - + # build for android if platform is android or all, or if triggered by a release if: github.event_name == 'release' || inputs.buildPlatform == 'all' || inputs.buildPlatform == 'android' @@ -87,21 +86,26 @@ jobs: with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - + - name: Install Node.js uses: actions/setup-node@v4 with: node-version: '22.x' + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + - name: Install Dependencies - run: npm install + run: pnpm install - name: Write Google Maps API Key to app.json run: | sed -i "s/{{GOOGLE_MAPS_KEY}}/$GOOGLE_MAPS_KEY/g" app.json env: GOOGLE_MAPS_KEY: ${{ secrets.GOOGLE_MAPS_KEY }} - + - name: Expo Prebuild [Android] run: | npx expo prebuild --platform android --npm @@ -129,16 +133,16 @@ jobs: KEY_PASSWORD: ${{ secrets.AAB_PASSWORD }} STORE_PASSWORD: ${{ secrets.AAB_PASSWORD }} KEY_PATH: ${{ github.workspace }}/android/app/maroon-rides-release-key.jks - + GOOGLE_PLAY_KEY: ${{ secrets.GOOGLE_PLAY_KEY }} - PACKAGE_NAME: "com.maroonrides.maroonrides" + PACKAGE_NAME: 'com.maroonrides.maroonrides' APP_GRADLE_FILE: ${{ github.workspace }}/android/app/build.gradle - + - uses: actions/upload-artifact@v4 with: name: app-bundle.aab path: ./android/app/build/outputs/bundle/release/app-release.aab - + - name: Set outputs id: git_sha run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT @@ -148,4 +152,4 @@ jobs: env: GOOGLE_PLAY_KEY: ${{ secrets.GOOGLE_PLAY_KEY }} RELEASE_NAME: ${{ github.event.release.tag_name || steps.git_sha.outputs.sha_short }} - PACKAGE_NAME: "com.maroonrides.maroonrides" + PACKAGE_NAME: 'com.maroonrides.maroonrides' diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 00000000..42eb67b5 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,52 @@ +name: Fix formatting + +on: + pull_request: + types: [labeled] + +permissions: {} + +jobs: + format: + runs-on: ubuntu-latest + if: ${{ github.event.label.name == 'ci:format' }} + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Install dependencies + run: pnpm install + + - name: Lint with expo + run: npx expo lint --fix + + - name: Commit and push + uses: EndBug/add-and-commit + with: + default_author: github_actions + message: 'chore: fix formatting' + + - name: Remove label + uses: actions/github-script + if: always() + with: + script: | + github.rest.issues.removeLabel({ + issue_number: context.payload.pull_request.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: 'ci:format' + }) diff --git a/.github/workflows/tsc.yml b/.github/workflows/lint.yml similarity index 58% rename from .github/workflows/tsc.yml rename to .github/workflows/lint.yml index 61e5e803..558d0eb5 100644 --- a/.github/workflows/tsc.yml +++ b/.github/workflows/lint.yml @@ -3,12 +3,14 @@ name: TypeScript Linting on: push: paths-ignore: - - ".github/**" - - "fastlane/**" + - '.github/**' + - 'fastlane/**' + branches: + - main pull_request: jobs: - tsc: + lint: runs-on: ubuntu-latest steps: @@ -20,9 +22,16 @@ jobs: with: node-version: 22 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + - name: Install dependencies - run: npm install + run: pnpm install + + - name: Lint with expo + run: npx expo lint --max-warnings 0 - name: Run TypeScript checks run: npx tsc - diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..72275c35 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "trailingComma": "all", + "tabWidth": 2, + "singleQuote": true +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..940260d8 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["dbaeumer.vscode-eslint"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..4dd6b642 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "always" + }, + "eslint.rules.customizations": [ + { + "rule": "prettier/prettier", + "severity": "off" + } + ] +} diff --git a/README.md b/README.md index 1ee12b5c..dad61378 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Maroon Rides + [![API Canary](https://github.com/Maroon-Rides/canary/actions/workflows/canary.yml/badge.svg)](https://github.com/Maroon-Rides/canary/actions/workflows/canary.yml) We are building the best native mobile app for the Texas A&M University bus system. - ![maroonrides_header](https://github.com/user-attachments/assets/f85ba3ce-9ad9-49cc-aebc-f26d1cee1105) diff --git a/app.d.ts b/app.d.ts index ca151bd0..0b97fbb0 100644 --- a/app.d.ts +++ b/app.d.ts @@ -1 +1 @@ -declare module 'moment-strftime'; \ No newline at end of file +declare module 'moment-strftime'; diff --git a/app.json b/app.json index 94ec33d2..a6fbb3e5 100644 --- a/app.json +++ b/app.json @@ -1,11 +1,12 @@ { "expo": { "name": "Maroon Rides", - "slug": "Maroon Rides", + "slug": "Maroon_Rides", "version": "1.6.5", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "automatic", + "newArchEnabled": false, "scheme": "myapp", "assetBundlePatterns": [ "**/*" @@ -82,7 +83,6 @@ "bundler": "metro" }, "plugins": [ - "@config-plugins/detox", [ "@bacons/apple-targets", { @@ -97,16 +97,21 @@ "locationWhenInUsePermission": "Allow location for map display and route planning" } ], - ["expo-build-properties", { - "android": { + [ + "expo-build-properties", + { + "android": { "compileSdkVersion": 35, "targetSdkVersion": 35, "buildToolsVersion": "35.0.0" - }, - "ios": { - "deploymentTarget": "13.4" + }, + "ios": { + "deploymentTarget": "15.1" + } } - }] + ], + "expo-font", + "expo-router" ] } -} +} \ No newline at end of file diff --git a/app/_layout.tsx b/app/_layout.tsx index 0d75032b..86a9d1ec 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,7 +1,11 @@ import { Stack } from 'expo-router'; export default function Layout() { - return ; + return ( + + ); } diff --git a/app/components/Error.tsx b/app/components/Error.tsx index 4ee7bc13..3ff4bac5 100644 --- a/app/components/Error.tsx +++ b/app/components/Error.tsx @@ -1,15 +1,23 @@ -import useAppStore from "../data/app_state"; -import React from "react"; +import useAppStore from '../data/app_state'; +import React from 'react'; import { SafeAreaView, Text } from 'react-native'; const Error: React.FC = () => { - const theme = useAppStore((state) => state.theme); + const theme = useAppStore((state) => state.theme); - return ( - - Something went wrong! - - ) -} + return ( + + Something went wrong! + + ); +}; export default Error; diff --git a/app/components/map/BusCallout.tsx b/app/components/map/BusCallout.tsx index 92cfda9c..6ff2a33d 100644 --- a/app/components/map/BusCallout.tsx +++ b/app/components/map/BusCallout.tsx @@ -1,84 +1,114 @@ -import React, { memo } from 'react' -import { View, Text, Platform } from 'react-native' -import { Callout } from 'react-native-maps' +import React, { memo } from 'react'; +import { View, Text, Platform } from 'react-native'; +import { Callout } from 'react-native-maps'; import { IAmenity } from '../../../utils/interfaces'; -import BusIcon from '../ui/BusIcon' +import BusIcon from '../ui/BusIcon'; import AmenityRow from '../ui/AmenityRow'; interface Props { - directionName: string - fullPercentage: number - amenities: IAmenity[] - tintColor: string - routeName: string - busId: string - speed: number + directionName: string; + fullPercentage: number; + amenities: IAmenity[]; + tintColor: string; + routeName: string; + busId: string; + speed: number; } // Bus callout with amentities -const BusCallout: React.FC = ({ directionName, fullPercentage, amenities, tintColor, routeName, busId, speed }) => { - - return ( - = ({ + directionName, + fullPercentage, + amenities, + tintColor, + routeName, + busId, + speed, +}) => { + return ( + + + - - - - - - ID: - {busId} - - - - - - - { directionName != "" && - - To: - {directionName} - - } - - - {fullPercentage}% Full - + + + + ID: + {busId} + + + + + - - {speed.toFixed(0)} MPH - - - - - ) -} + {directionName !== '' && ( + + To: + {directionName} + + )} + + + {fullPercentage}% Full + + + + {speed.toFixed(0)} MPH + + + + + ); +}; -export default memo(BusCallout); \ No newline at end of file +export default memo(BusCallout); diff --git a/app/components/map/MapView.tsx b/app/components/map/MapView.tsx index 3fedf8d7..e14856ce 100644 --- a/app/components/map/MapView.tsx +++ b/app/components/map/MapView.tsx @@ -1,447 +1,504 @@ -import React, { useEffect, useRef, useState } from "react"; -import { Dimensions, Platform, TouchableOpacity, View } from "react-native"; -import MapView, { LatLng, Polyline, Region } from 'react-native-maps'; -import * as Location from 'expo-location'; +import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; -import { IMapRoute, RoutePlanMapMarker, RoutePlanPolylinePoint } from "../../../utils/interfaces"; -import useAppStore from "../../data/app_state"; -import BusMarker from "./markers/BusMarker"; -import StopMarker from "./markers/StopMarker"; -import { useVehicles } from "../../data/api_query"; -import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; -import { decode } from "@googlemaps/polyline-codec"; -import RoutePlanMarker from "./markers/RoutePlanMarker"; -import { DarkGoogleMaps } from "app/theme"; +import { decode } from '@googlemaps/polyline-codec'; +import { DarkGoogleMaps } from 'app/theme'; +import * as Location from 'expo-location'; +import React, { useEffect, useRef, useState } from 'react'; +import { Dimensions, Platform, TouchableOpacity, View } from 'react-native'; +import MapView, { LatLng, Polyline, Region } from 'react-native-maps'; +import { + IMapRoute, + RoutePlanMapMarker, + RoutePlanPolylinePoint, +} from '../../../utils/interfaces'; +import { useVehicles } from '../../data/api_query'; +import useAppStore from '../../data/app_state'; +import BusMarker from './markers/BusMarker'; +import RoutePlanMarker from './markers/RoutePlanMarker'; +import StopMarker from './markers/StopMarker'; const Map: React.FC = () => { - const mapViewRef = useRef(null); - const setSelectedDirection = useAppStore(state => state.setSelectedRouteDirection); - const selectedRoute = useAppStore((state) => state.selectedRoute); - const setSelectedRoute = useAppStore((state) => state.setSelectedRoute); - const setDrawnRoutes = useAppStore((state) => state.setDrawnRoutes); - const presentSheet = useAppStore((state) => state.presentSheet); - const drawnRoutes = useAppStore((state) => state.drawnRoutes); - const setZoomToStopLatLng = useAppStore((state) => state.setZoomToStopLatLng); - const selectedRouteDirection = useAppStore(state => state.selectedRouteDirection); - const selectedRoutePlan = useAppStore(state => state.selectedRoutePlan); - const selectedRoutePlanPathPart = useAppStore(state => state.selectedRoutePlanPathPart); - const theme = useAppStore((state) => state.theme); - const poppedUpStopCallout = useAppStore((state) => state.poppedUpStopCallout); - - const [isViewCenteredOnUser, setIsViewCenteredOnUser] = useState(false); - - const [selectedRoutePlanPath, setSelectedRoutePlanPath] = useState([]); - const [highlightedRoutePlanPath, setHighlightedRoutePlanPath] = useState([]); - const [fadedRoutePlanPath, setFadedRoutePlanPath] = useState([]); - const [routePlanMapMarkers, setRoutePlanMapMarkers] = useState([]); - - const { data: buses } = useVehicles(selectedRoute?.key ?? ""); - - const defaultMapRegion: Region = { - latitude: 30.6060, - longitude: -96.3462, - latitudeDelta: 0.10, - longitudeDelta: 0.01 - }; - - function selectRoute(route: IMapRoute, directionKey: string) { + const mapViewRef = useRef(null); + const setSelectedDirection = useAppStore( + (state) => state.setSelectedRouteDirection, + ); + const selectedRoute = useAppStore((state) => state.selectedRoute); + const setSelectedRoute = useAppStore((state) => state.setSelectedRoute); + const setDrawnRoutes = useAppStore((state) => state.setDrawnRoutes); + const presentSheet = useAppStore((state) => state.presentSheet); + const drawnRoutes = useAppStore((state) => state.drawnRoutes); + const setZoomToStopLatLng = useAppStore((state) => state.setZoomToStopLatLng); + const selectedRouteDirection = useAppStore( + (state) => state.selectedRouteDirection, + ); + const selectedRoutePlan = useAppStore((state) => state.selectedRoutePlan); + const selectedRoutePlanPathPart = useAppStore( + (state) => state.selectedRoutePlanPathPart, + ); + const theme = useAppStore((state) => state.theme); + const poppedUpStopCallout = useAppStore((state) => state.poppedUpStopCallout); + + const [isViewCenteredOnUser, setIsViewCenteredOnUser] = useState(false); + + const [selectedRoutePlanPath, setSelectedRoutePlanPath] = useState< + RoutePlanPolylinePoint[] + >([]); + const [highlightedRoutePlanPath, setHighlightedRoutePlanPath] = useState< + RoutePlanPolylinePoint[] + >([]); + const [fadedRoutePlanPath, setFadedRoutePlanPath] = useState< + RoutePlanPolylinePoint[][] + >([]); + const [routePlanMapMarkers, setRoutePlanMapMarkers] = useState< + RoutePlanMapMarker[] + >([]); + + const { data: buses } = useVehicles(selectedRoute?.key ?? ''); + + const defaultMapRegion: Region = { + latitude: 30.606, + longitude: -96.3462, + latitudeDelta: 0.1, + longitudeDelta: 0.01, + }; + + function selectRoute(route: IMapRoute, directionKey: string) { + if (selectedRouteDirection !== directionKey) + setSelectedDirection(directionKey); + + if (selectedRoute?.key === route.key) return; + + setSelectedRoute(route); + setDrawnRoutes([route]); + presentSheet('routeDetails'); + } + + // center the view on the drawn routes + useEffect(() => { + centerViewOnRoutes(); + }, [drawnRoutes, selectedRoutePlanPath]); + + // Generate the path points for the selected route plan + useEffect(() => { + if (!selectedRoutePlan) { + setSelectedRoutePlanPath([]); + setHighlightedRoutePlanPath([]); + setFadedRoutePlanPath([]); + setRoutePlanMapMarkers([]); + return; + } - if (selectedRouteDirection !== directionKey) setSelectedDirection(directionKey); + let polyline: RoutePlanPolylinePoint[] = []; + selectedRoutePlan?.instructions.forEach((instruction, index) => { + if (instruction.polyline) { + decode(instruction.polyline).forEach((point) => { + polyline.push({ + latitude: point[0], + longitude: point[1], + stepIndex: index, + pathIndex: polyline.length, + }); + }); + } + }); + + setSelectedRoutePlanPath(polyline); + + // clear the highlighted path if no route plan is selected - if (selectedRoute?.key === route.key) return; + setHighlightedRoutePlanPath(polyline); + setFadedRoutePlanPath([]); - setSelectedRoute(route); - setDrawnRoutes([route]); - presentSheet("routeDetails"); - + if (polyline.length === 0) { + setRoutePlanMapMarkers([]); + return; } - // center the view on the drawn routes - useEffect(() => { - centerViewOnRoutes(); - }, [drawnRoutes, selectedRoutePlanPath]); - - // Generate the path points for the selected route plan - useEffect(() => { - if (!selectedRoutePlan) { - setSelectedRoutePlanPath([]); - setHighlightedRoutePlanPath([]); - setFadedRoutePlanPath([]); - setRoutePlanMapMarkers([]); - return; - } + setRoutePlanMapMarkers([ + { + icon: , + latitude: polyline[polyline.length - 1]!.latitude, + longitude: polyline[polyline.length - 1]!.longitude, + }, + { + icon: , + latitude: polyline[0]!.latitude, + longitude: polyline[0]!.longitude, + isOrigin: true, + }, + ]); + }, [selectedRoutePlan]); + + // Adjust the zoom and the path to show the selected part of the route plan + useEffect(() => { + if (!selectedRoutePlan) return; + + // filter the selected route plan path to only show the selected part + let highlighted: RoutePlanPolylinePoint[] = []; + if (selectedRoutePlanPathPart === -1) { + highlighted = selectedRoutePlanPath; + setHighlightedRoutePlanPath(selectedRoutePlanPath); + setFadedRoutePlanPath([]); + centerViewOnRoutes(); + } - var polyline: RoutePlanPolylinePoint[] = []; - selectedRoutePlan?.instructions.forEach((instruction, index) => { - if (instruction.polyline) { - decode(instruction.polyline).forEach((point) => { - polyline.push({ - latitude: point[0], - longitude: point[1], - stepIndex: index, - pathIndex: polyline.length - }); - }); - } - }) - - setSelectedRoutePlanPath(polyline); - - // clear the highlighted path if no route plan is selected - - setHighlightedRoutePlanPath(polyline); - setFadedRoutePlanPath([]); - - if (polyline.length === 0) { - setRoutePlanMapMarkers([]); - return; + // filter the selected route plan path to only show the selected part + if (selectedRoutePlanPathPart >= 0) { + highlighted = selectedRoutePlanPath.filter( + (point) => point.stepIndex === selectedRoutePlanPathPart, + ); + setHighlightedRoutePlanPath(highlighted); + + // break the path into two parts, before and after the selected part + let faded: RoutePlanPolylinePoint[][] = [[], []]; + selectedRoutePlanPath.forEach((point) => { + if (point.stepIndex < selectedRoutePlanPathPart) { + faded[0]!.push(point); + } else if (point.stepIndex > selectedRoutePlanPathPart) { + faded[1]!.push(point); } + }); - setRoutePlanMapMarkers([ + setFadedRoutePlanPath(faded); + } + + if (highlighted.length === 0) { + // get the last point of the path index - 1 and zoom to that point + // do this by finding the last point that has a stepIndex of selectedRoutePlanPathPart - 1 + const lastPoint = [...selectedRoutePlanPath] + .reverse() + .find((point) => point.stepIndex === selectedRoutePlanPathPart - 1); + + if (lastPoint) { + // if its the last location, show the finish flag + if (lastPoint.pathIndex === selectedRoutePlanPath.length - 1) { + setRoutePlanMapMarkers([ { - icon: , - latitude: polyline[polyline.length-1]!.latitude, - longitude: polyline[polyline.length-1]!.longitude + icon: ( + + ), + latitude: lastPoint.latitude, + longitude: lastPoint.longitude, }, - { - icon: , - latitude: polyline[0]!.latitude, - longitude: polyline[0]!.longitude, - isOrigin: true - } - ]); - }, [selectedRoutePlan]) - - // Adjust the zoom and the path to show the selected part of the route plan - useEffect(() => { - if (!selectedRoutePlan) return; - - // filter the selected route plan path to only show the selected part - var highlighted: RoutePlanPolylinePoint[] = []; - if (selectedRoutePlanPathPart === -1) { - highlighted = selectedRoutePlanPath; - setHighlightedRoutePlanPath(selectedRoutePlanPath); - setFadedRoutePlanPath([]); - centerViewOnRoutes(); - } - - // filter the selected route plan path to only show the selected part - if (selectedRoutePlanPathPart >= 0) { - highlighted = selectedRoutePlanPath.filter((point) => point.stepIndex === selectedRoutePlanPathPart); - setHighlightedRoutePlanPath(highlighted); - - // break the path into two parts, before and after the selected part - var faded: RoutePlanPolylinePoint[][] = [[], []]; - selectedRoutePlanPath.forEach((point) => { - if (point.stepIndex < selectedRoutePlanPathPart) { - faded[0]!.push(point); - } else if (point.stepIndex > selectedRoutePlanPathPart) { - faded[1]!.push(point); - } - }); - - setFadedRoutePlanPath(faded); - } - - if (highlighted.length === 0) { - // get the last point of the path index - 1 and zoom to that point - // do this by finding the last point that has a stepIndex of selectedRoutePlanPathPart - 1 - const lastPoint = [...selectedRoutePlanPath].reverse().find((point) => point.stepIndex === selectedRoutePlanPathPart - 1); - - if (lastPoint) { - // if its the last location, show the finish flag - if (lastPoint.pathIndex == selectedRoutePlanPath.length - 1) { - setRoutePlanMapMarkers([ - { - icon: , - latitude: lastPoint.latitude, - longitude: lastPoint.longitude - } - ]); - } else { - setRoutePlanMapMarkers([ - { - icon: , - latitude: lastPoint.latitude, - longitude: lastPoint.longitude - } - ]); - } - - mapViewRef.current?.animateToRegion({ - latitude: lastPoint.latitude - .002, - longitude: lastPoint.longitude, - latitudeDelta: 0.005, - longitudeDelta: 0.005 - }); - } - - return; + ]); } else { - setRoutePlanMapMarkers([ - { - icon:, - latitude: highlighted[highlighted.length-1]!.latitude, - longitude: highlighted[highlighted.length-1]!.longitude - }, - { - icon: , - latitude: highlighted[0]!.latitude, - longitude: highlighted[0]!.longitude, - isOrigin: true - } - ]); + setRoutePlanMapMarkers([ + { + icon: ( + + ), + latitude: lastPoint.latitude, + longitude: lastPoint.longitude, + }, + ]); } - // animate to the selected part of the route plan - mapViewRef.current?.fitToCoordinates(highlighted, { - edgePadding: { - top: Dimensions.get("window").height * 0.05, - right: 40, - bottom: Dimensions.get("window").height * 0.45 + 8, - left: 40 - }, - animated: true + mapViewRef.current?.animateToRegion({ + latitude: lastPoint.latitude - 0.002, + longitude: lastPoint.longitude, + latitudeDelta: 0.005, + longitudeDelta: 0.005, }); - }, [selectedRoutePlanPathPart]) - - // handle weird edge case where map does not pick up on the initial region - useEffect(() => { - mapViewRef.current?.animateToRegion(defaultMapRegion); + } + + return; + } else { + setRoutePlanMapMarkers([ + { + icon: ( + + ), + latitude: highlighted[highlighted.length - 1]!.latitude, + longitude: highlighted[highlighted.length - 1]!.longitude, + }, + { + icon: ( + + ), + latitude: highlighted[0]!.latitude, + longitude: highlighted[0]!.longitude, + isOrigin: true, + }, + ]); + } - setZoomToStopLatLng((lat, lng) => { - // Animate map to the current location - const region = { - latitude: lat - .002, - longitude: lng, - latitudeDelta: 0.005, - longitudeDelta: 0.005 - }; + // animate to the selected part of the route plan + mapViewRef.current?.fitToCoordinates(highlighted, { + edgePadding: { + top: Dimensions.get('window').height * 0.05, + right: 40, + bottom: Dimensions.get('window').height * 0.45 + 8, + left: 40, + }, + animated: true, + }); + }, [selectedRoutePlanPathPart]); + + // handle weird edge case where map does not pick up on the initial region + useEffect(() => { + mapViewRef.current?.animateToRegion(defaultMapRegion); + + setZoomToStopLatLng((lat, lng) => { + // Animate map to the current location + const region = { + latitude: lat - 0.002, + longitude: lng, + latitudeDelta: 0.005, + longitudeDelta: 0.005, + }; + + mapViewRef.current?.animateToRegion(region, 250); + }); + }, []); + + const centerViewOnRoutes = () => { + let coords: LatLng[] = []; + + if (selectedRoute) { + selectedRoute.patternPaths.forEach((path) => { + path.patternPoints.forEach((point) => { + coords.push({ + latitude: point.latitude, + longitude: point.longitude, + }); + }); + }); + } - mapViewRef.current?.animateToRegion(region, 250); - }) + drawnRoutes.forEach((route) => { + route.patternPaths.forEach((path) => { + path.patternPoints.forEach((point) => { + coords.push({ + latitude: point.latitude, + longitude: point.longitude, + }); + }); + }); + }); + + // add the selected route plan path to coords + selectedRoutePlanPath.forEach((point) => { + coords.push({ + latitude: point.latitude, + longitude: point.longitude, + }); + }); + + if (coords.length > 0) { + mapViewRef.current?.fitToCoordinates(coords, { + edgePadding: { + top: Dimensions.get('window').height * 0.05, + right: 40, + bottom: Dimensions.get('window').height * 0.45 + 8, + left: 40, + }, + animated: true, + }); + } - }, []); + setIsViewCenteredOnUser(false); + }; + const recenterView = async () => { + // Request location permissions + const { status } = await Location.requestForegroundPermissionsAsync(); + // Check if permission is granted + if (status !== 'granted') { + return; + } - const centerViewOnRoutes = () => { - let coords: LatLng[] = []; + // Get current location + const location = await Location.getCurrentPositionAsync({ + accuracy: Location.Accuracy.Balanced, + timeInterval: 2, + }); + + // Animate map to the current location + const region = { + latitude: location.coords.latitude - 0.002, + longitude: location.coords.longitude, + latitudeDelta: 0.01, + longitudeDelta: 0.01, + }; - if (selectedRoute) { - selectedRoute.patternPaths.forEach((path) => { - path.patternPoints.forEach((point) => { - coords.push({ - latitude: point.latitude, - longitude: point.longitude, - }); - }); - }); + mapViewRef.current?.animateToRegion(region, 250); + + setIsViewCenteredOnUser(true); + }; + + return ( + <> + setIsViewCenteredOnUser(false)} + // this deprcation is ok, we only use it on android + maxZoomLevel={Platform.OS === 'android' ? 18 : undefined} + showsMyLocationButton={false} // we have our own + // fix dark mode android map syling + customMapStyle={ + Platform.OS === 'android' && theme.mode === 'dark' + ? DarkGoogleMaps + : [] } + > + {/* Route Polylines */} + {drawnRoutes.map((drawnRoute) => { + const coordDirections: { [directionId: string]: LatLng[] } = {}; - drawnRoutes.forEach((route) => { - route.patternPaths.forEach((path) => { - path.patternPoints.forEach((point) => { - coords.push({ - latitude: point.latitude, - longitude: point.longitude - }); - }) - }) - }); + drawnRoute.patternPaths.forEach((path) => { + path.patternPoints.forEach((point) => { + coordDirections[path.directionKey] = + coordDirections[path.directionKey] ?? []; - // add the selected route plan path to coords - selectedRoutePlanPath.forEach((point) => { - coords.push({ + coordDirections[path.directionKey]!.push({ latitude: point.latitude, - longitude: point.longitude - }); - }) - - if (coords.length > 0) { - mapViewRef.current?.fitToCoordinates(coords, { - edgePadding: { - top: Dimensions.get("window").height * 0.05, - right: 40, - bottom: Dimensions.get("window").height * 0.45 + 8, - left: 40 - }, - animated: true + longitude: point.longitude, + }); }); - } - - setIsViewCenteredOnUser(false); - } - - const recenterView = async () => { - // Request location permissions - const { status } = await Location.requestForegroundPermissionsAsync() - - // Check if permission is granted - if (status !== 'granted') { return }; - - // Get current location - const location = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced, timeInterval: 2 }); - - // Animate map to the current location - const region = { - latitude: location.coords.latitude - .002, - longitude: location.coords.longitude, - latitudeDelta: 0.01, - longitudeDelta: 0.01 - }; - - mapViewRef.current?.animateToRegion(region, 250); - - setIsViewCenteredOnUser(true); - } - - return ( - <> - setIsViewCenteredOnUser(false)} - // this deprcation is ok, we only use it on android - maxZoomLevel={Platform.OS == "android" ? 18 : undefined} - showsMyLocationButton={false} // we have our own - // fix dark mode android map syling - customMapStyle={Platform.OS == "android" && theme.mode == "dark" ? DarkGoogleMaps : []} - > - {/* Route Polylines */} - {drawnRoutes.map((drawnRoute) => { - const coordDirections: { [directionId: string]: LatLng[]; } = {}; - - drawnRoute.patternPaths.forEach((path) => { - path.patternPoints.forEach((point) => { - coordDirections[path.directionKey] = coordDirections[path.directionKey] ?? []; - - coordDirections[path.directionKey]!.push({ - latitude: point.latitude, - longitude: point.longitude, - }) - }) - }) - - const lineColor = drawnRoute.directionList[0]?.lineColor ?? "#FFFF"; - - return ( - Object.keys(coordDirections).map((directionId) => { - const active = directionId === selectedRouteDirection || selectedRouteDirection == null - return ( - selectRoute(drawnRoute, directionId)} - style={{ zIndex: active ? 600 : 300, - elevation: active ? 600 : 300 }} - /> - ) - }) - ) - })} - - {/* Stops */} - {selectedRoute && selectedRoute?.patternPaths.flatMap((patternPath, index1) => ( - patternPath.patternPoints.map((patternPoint, index2) => { - const stop = patternPoint.stop - - // if it is the end of the route, dont put a marker - if (index2 == patternPath.patternPoints.length-1) return - - if (stop) { - return ( - - ); - } - - return null; - }) - ))} - - {/* Route Plan Highlighted */} - {selectedRoutePlan && - - } - - {/* Route Plan Faded */} - {selectedRoutePlan && fadedRoutePlanPath.map((path, index) => { - return - })} - - {/* Route Plan Markers */} - {selectedRoutePlan && routePlanMapMarkers.map((marker, index) => { - return ( - - ) - })} - - {/* Buses */} - {selectedRoute && buses?.map((bus) => { - const color = selectedRoute.directionList[0]?.lineColor ?? "#500000" - return ( - - ) - })} - - - {/* map buttons */} - - recenterView()} style={{ padding: 12 }}> - {isViewCenteredOnUser ? - - : - + }); + + const lineColor = drawnRoute.directionList[0]?.lineColor ?? '#FFFF'; + + return Object.keys(coordDirections).map((directionId) => { + const active = + directionId === selectedRouteDirection || + selectedRouteDirection === null; + return ( + selectRoute(drawnRoute, directionId)} + style={{ + zIndex: active ? 600 : 300, + elevation: active ? 600 : 300, + }} + /> + ); + }); + })} + + {/* Stops */} + {selectedRoute && + selectedRoute?.patternPaths.flatMap((patternPath, index1) => + patternPath.patternPoints.map((patternPoint, index2) => { + const stop = patternPoint.stop; + + // if it is the end of the route, dont put a marker + if (index2 === patternPath.patternPoints.length - 1) return; + + if (stop) { + return ( + - - - - ) -} - -export default Map; \ No newline at end of file + /> + ); + } + + return null; + }), + )} + + {/* Route Plan Highlighted */} + {selectedRoutePlan && ( + + )} + + {/* Route Plan Faded */} + {selectedRoutePlan && + fadedRoutePlanPath.map((path, index) => { + return ( + + ); + })} + + {/* Route Plan Markers */} + {selectedRoutePlan && + routePlanMapMarkers.map((marker, index) => { + return ( + + ); + })} + + {/* Buses */} + {selectedRoute && + buses?.map((bus) => { + const color = + selectedRoute.directionList[0]?.lineColor ?? '#500000'; + return ( + + ); + })} + + + {/* map buttons */} + + recenterView()} + style={{ padding: 12 }} + > + {isViewCenteredOnUser ? ( + + ) : ( + + )} + + + + ); +}; + +export default Map; diff --git a/app/components/map/StopCallout.tsx b/app/components/map/StopCallout.tsx index 48798d33..e2daaa77 100644 --- a/app/components/map/StopCallout.tsx +++ b/app/components/map/StopCallout.tsx @@ -1,95 +1,145 @@ -import React, { memo } from 'react' -import { View, Text, ActivityIndicator, Platform } from 'react-native' -import { Callout } from 'react-native-maps' -import BusIcon from '../ui/BusIcon' -import { IMapRoute, IStop } from '../../../utils/interfaces' -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' +import { useStopEstimate } from 'app/data/api_query'; +import useAppStore from 'app/data/app_state'; +import { lightMode } from 'app/theme'; +import moment from 'moment'; +import React, { memo } from 'react'; +import { ActivityIndicator, Platform, Text, View } from 'react-native'; +import { Callout } from 'react-native-maps'; +import { IMapRoute, IStop } from '../../../utils/interfaces'; +import AmenityRow from '../ui/AmenityRow'; +import BusIcon from '../ui/BusIcon'; +import CalloutTimeBubble from '../ui/CalloutTimeBubble'; interface Props { - stop: IStop - tintColor: string - route: IMapRoute - direction: string + stop: IStop; + tintColor: string; + route: IMapRoute; + direction: string; } // Stop callout with time bubbles -const StopCallout: React.FC = ({ stop, tintColor, route, direction }) => { +const StopCallout: React.FC = ({ + stop, + tintColor, + route, + direction, +}) => { + const scrollToStop = useAppStore((state) => state.scrollToStop); + const setSelectedRouteDirection = useAppStore( + (state) => state.setSelectedRouteDirection, + ); - const scrollToStop = useAppStore(state => state.scrollToStop); - const setSelectedRouteDirection = useAppStore(state => state.setSelectedRouteDirection) + const { data: estimate, isLoading } = useStopEstimate( + route.key, + direction, + stop.stopCode, + ); - const { data: estimate, isLoading } = useStopEstimate(route.key, direction, stop.stopCode); + return ( + { + setSelectedRouteDirection(direction); + scrollToStop(stop); + }} + > + + + + + {stop.name} + + + - return ( - + ) : estimate?.routeDirectionTimes.length !== 0 && + estimate?.routeDirectionTimes[0]?.nextDeparts.length !== 0 ? ( + { - setSelectedRouteDirection(direction) - scrollToStop(stop) + > + + {estimate?.routeDirectionTimes[0]?.nextDeparts.map( + (departureTime, index) => { + const date = moment( + departureTime.estimatedDepartTimeUtc ?? + departureTime.scheduledDepartTimeUtc ?? + '', + ); + const relative = date.diff(moment(), 'minutes'); + return ( + + ); + }, + )} + + + ) : ( + - - - - {stop.name} - - - - { isLoading ? - - : ( estimate?.routeDirectionTimes.length != 0 && 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 ( - - ) - })} - - - : - No upcoming departures - ) - } - - - ) -} + > + No upcoming departures + + )} + + + ); +}; -export default memo(StopCallout); \ No newline at end of file +export default memo(StopCallout); diff --git a/app/components/map/mapIcons/BusMapIcon.tsx b/app/components/map/mapIcons/BusMapIcon.tsx index 97584bd0..8e88d6c8 100644 --- a/app/components/map/mapIcons/BusMapIcon.tsx +++ b/app/components/map/mapIcons/BusMapIcon.tsx @@ -1,43 +1,55 @@ -import React from 'react' -import { View } from 'react-native' +import React from 'react'; +import { View } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { getLighterColor } from 'app/utils'; interface Props { - heading: number, - tintColor: string, - active: boolean + heading: number; + tintColor: string; + active: boolean; } // Bus icon thats show on map const BusMapIcon: React.FC = ({ heading, tintColor, active }) => { // Calculate the rotation angle based on the bearing of the bus const getRotationProp = (bearing: number | undefined) => { - return [{ rotate: bearing !== undefined ? `${Math.round(bearing) - 135}deg` : '0deg' }] + return [ + { + rotate: + bearing !== undefined ? `${Math.round(bearing) - 135}deg` : '0deg', + }, + ]; }; const borderColor = active ? getLighterColor(tintColor) : undefined; return ( - - + + - ) -} + ); +}; // Not memoizing this component since the bearing changes when the bus moves -export default BusMapIcon; \ No newline at end of file +export default BusMapIcon; diff --git a/app/components/map/markers/BusMarker.tsx b/app/components/map/markers/BusMarker.tsx index d1aeaaa9..034155f1 100644 --- a/app/components/map/markers/BusMarker.tsx +++ b/app/components/map/markers/BusMarker.tsx @@ -7,61 +7,72 @@ import useAppStore from '../../../data/app_state'; import { Platform } from 'react-native'; interface Props { - bus: IVehicle, - tintColor: string, - routeName: string + bus: IVehicle; + tintColor: string; + routeName: string; } // Bus Marker with icon and callout const BusMarker: React.FC = ({ bus, tintColor, routeName }) => { - const selectedRouteDirection = useAppStore(state => state.selectedRouteDirection); - const setSelectedDirection = useAppStore(state => state.setSelectedRouteDirection); + const selectedRouteDirection = useAppStore( + (state) => state.selectedRouteDirection, + ); + const setSelectedDirection = useAppStore( + (state) => state.setSelectedRouteDirection, + ); - //if direction is not selected and route is inactive, then call setSelectedDirection w/ parameter bus.directionKey - const busDefaultDirection = () => { - if (selectedRouteDirection !== bus.directionKey) { - setSelectedDirection(bus.directionKey); - } + //if direction is not selected and route is inactive, then call setSelectedDirection w/ parameter bus.directionKey + const busDefaultDirection = () => { + if (selectedRouteDirection !== bus.directionKey) { + setSelectedDirection(bus.directionKey); } - - - return ( - busDefaultDirection()} - > - {/* Bus Icon on Map*/} - + Platform.OS === 'android' && { + width: 42, + height: 42, + justifyContent: 'center', + alignItems: 'center', + }, + ]} + onPress={() => busDefaultDirection()} + > + {/* Bus Icon on Map*/} + - - - ); + + + ); }; export default memo(BusMarker); diff --git a/app/components/map/markers/RoutePlanMarker.tsx b/app/components/map/markers/RoutePlanMarker.tsx index 29a74ff0..cedc6d01 100644 --- a/app/components/map/markers/RoutePlanMarker.tsx +++ b/app/components/map/markers/RoutePlanMarker.tsx @@ -6,69 +6,73 @@ import { getLighterColor } from 'app/utils'; import useAppStore from 'app/data/app_state'; interface Props { - marker: RoutePlanMapMarker + marker: RoutePlanMapMarker; } // Stop marker with callout const RoutePlanMarker: React.FC = ({ marker }) => { - const markerRef = React.useRef(null); - const theme = useAppStore(state => state.theme); + const markerRef = React.useRef(null); + const theme = useAppStore((state) => state.theme); - return ( - + {marker.isOrigin ? ( + - { marker.isOrigin ? ( - - {marker.icon} - - ) : ( - - {marker.icon} - - )} - - ); + {marker.icon} + + ) : ( + + {marker.icon} + + )} + + ); }; export default memo(RoutePlanMarker); diff --git a/app/components/map/markers/StopMarker.tsx b/app/components/map/markers/StopMarker.tsx index cf48e46e..3483c098 100644 --- a/app/components/map/markers/StopMarker.tsx +++ b/app/components/map/markers/StopMarker.tsx @@ -8,64 +8,75 @@ import { getLighterColor } from 'app/utils'; import useAppStore from '../../../data/app_state'; interface Props { - point: IPatternPoint - tintColor: string - route: IMapRoute - direction: string - isCalloutShown?: boolean - active: boolean + point: IPatternPoint; + tintColor: string; + route: IMapRoute; + direction: string; + isCalloutShown?: boolean; + active: boolean; } // Stop marker with callout -const StopMarker: React.FC = ({ point, tintColor, route, direction, isCalloutShown=false, active }) => { - const markerRef = React.useRef(null); - const setSelectedDirection = useAppStore(state => state.setSelectedRouteDirection); +const StopMarker: React.FC = ({ + point, + tintColor, + route, + direction, + isCalloutShown = false, + active, +}) => { + const markerRef = React.useRef(null); + const setSelectedDirection = useAppStore( + (state) => state.setSelectedRouteDirection, + ); - // If the global poppedUpStopCallout is the same as the current stop, show the callout on screen - useEffect(() => { - if (isCalloutShown) { - markerRef.current?.showCallout(); - } - }, [isCalloutShown]) + // If the global poppedUpStopCallout is the same as the current stop, show the callout on screen + useEffect(() => { + if (isCalloutShown) { + markerRef.current?.showCallout(); + } + }, [isCalloutShown]); - const defaultDirection = () => { - if (active == false) { - setSelectedDirection(direction); - } + const defaultDirection = () => { + if (active === false) { + setSelectedDirection(direction); } + }; - return ( - defaultDirection()} - > - - - - ); + return ( + defaultDirection()} + > + + + + ); }; export default memo(StopMarker); diff --git a/app/components/sheets/AlertDetail.tsx b/app/components/sheets/AlertDetail.tsx index b331b780..34a5380e 100644 --- a/app/components/sheets/AlertDetail.tsx +++ b/app/components/sheets/AlertDetail.tsx @@ -1,83 +1,117 @@ -import { BottomSheetModal, BottomSheetScrollView, BottomSheetView } from "@gorhom/bottom-sheet"; -import { View, TouchableOpacity, useWindowDimensions } from "react-native"; +import { BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet'; +import { View, TouchableOpacity, useWindowDimensions } from 'react-native'; import Ionicons from '@expo/vector-icons/Ionicons'; -import useAppStore from "../../data/app_state"; -import SheetHeader from "../ui/SheetHeader"; +import useAppStore from '../../data/app_state'; +import SheetHeader from '../ui/SheetHeader'; import RenderHtml from 'react-native-render-html'; -import { useRoutes } from "app/data/api_query"; -import { useState } from "react"; +import { useRoutes } from 'app/data/api_query'; +import { useState } from 'react'; +import { SheetProps } from 'app/utils'; +const AlertDetails: React.FC = ({ sheetRef }) => { + const snapPoints = ['25%', '45%', '85%']; + const alert = useAppStore((state) => state.selectedAlert); + const theme = useAppStore((state) => state.theme); + const setDrawnRoutes = useAppStore((state) => state.setDrawnRoutes); + const setSelectedRoute = useAppStore((state) => state.setSelectedRoute); + const oldSelectedRoute = useAppStore((state) => state.oldSelectedRoute); + const dismissSheet = useAppStore((state) => state.dismissSheet); -const AlertDetails: React.FC<{ sheetRef: React.RefObject }> = ({ sheetRef }) => { - const snapPoints = ['25%', '45%', '85%']; - const alert = useAppStore((state) => state.selectedAlert); - const theme = useAppStore((state) => state.theme); - const setDrawnRoutes = useAppStore((state) => state.setDrawnRoutes); - const setSelectedRoute = useAppStore((state) => state.setSelectedRoute); - const oldSelectedRoute = useAppStore((state) => state.oldSelectedRoute); - const dismissSheet = useAppStore((state) => state.dismissSheet); + const { data: routes } = useRoutes(); - const { data: routes } = useRoutes(); + const tagStyles = { + h3: { + fontSize: 32, + fontWeight: 'bold', + marginTop: 24, + marginBottom: 8, + color: theme.text, + }, + h6: { + fontSize: 20, + fontWeight: 'bold', + marginTop: 24, + marginBottom: 8, + color: theme.text, + }, + span: { fontWeight: 'bold' }, + ul: { + marginLeft: 16, + marginTop: 8, + padding: 8, + paddingLeft: 24, + backgroundColor: theme.secondaryBackground, + borderRadius: 8, + }, + div: { paddingBottom: 0, marginBottom: 0, color: theme.text }, + }; - const tagStyles = { - h3: { fontSize: 32, fontWeight: "bold", marginTop: 24, marginBottom: 8, color: theme.text }, - h6: { fontSize: 20, fontWeight: "bold", marginTop: 24, marginBottom: 8, color: theme.text }, - span: { fontWeight: "bold" }, - ul: { marginLeft: 16, marginTop: 8, padding: 8, paddingLeft: 24, backgroundColor: theme.secondaryBackground, borderRadius: 8 }, - div: { paddingBottom: 0, marginBottom: 0, color: theme.text } - }; + const [snap, _] = useState(1); - const [snap, _] = useState(1); - - const handleDismiss = () => { - if (oldSelectedRoute) { - setSelectedRoute(oldSelectedRoute); - setDrawnRoutes([oldSelectedRoute]); - } - dismissSheet("alertsDetail") + const handleDismiss = () => { + if (oldSelectedRoute) { + setSelectedRoute(oldSelectedRoute); + setDrawnRoutes([oldSelectedRoute]); } + dismissSheet('alertsDetail'); + }; - return ( - { - if (to === 1) { - const affectedRoutes = routes?.filter(route => route.directionList.flatMap(direction => direction.serviceInterruptionKeys).includes(Number(alert?.key))); - setDrawnRoutes(affectedRoutes ?? []) - } - }} - enablePanDownToClose={false} - > - - handleDismiss()}> - - - } - /> - - - - - - - - - - ); + return ( + { + if (to === 1) { + const affectedRoutes = routes?.filter((route) => + route.directionList + .flatMap((direction) => direction.serviceInterruptionKeys) + .includes(Number(alert?.key)), + ); + setDrawnRoutes(affectedRoutes ?? []); + } + }} + enablePanDownToClose={false} + enableDynamicSizing={false} + > + + handleDismiss()} + > + + + } + /> + + + + + + + ); }; -export default AlertDetails; \ No newline at end of file +export default AlertDetails; diff --git a/app/components/sheets/AlertList.tsx b/app/components/sheets/AlertList.tsx index 9df6030f..84cd3640 100644 --- a/app/components/sheets/AlertList.tsx +++ b/app/components/sheets/AlertList.tsx @@ -1,134 +1,157 @@ -import React, { memo, useEffect, useState } from "react"; -import { View, Text, TouchableOpacity } from "react-native"; -import { BottomSheetModal, BottomSheetView, BottomSheetFlatList } from "@gorhom/bottom-sheet"; +import React, { memo, useEffect, useState } from 'react'; +import { View, Text, TouchableOpacity } from 'react-native'; +import { BottomSheetModal, BottomSheetFlatList } from '@gorhom/bottom-sheet'; import Ionicons from '@expo/vector-icons/Ionicons'; -import useAppStore from "../../data/app_state"; -import SheetHeader from "../ui/SheetHeader"; -import { IMapRoute, IMapServiceInterruption } from "utils/interfaces"; -import { useServiceInterruptions } from "app/data/api_query"; - -interface SheetProps { - sheetRef: React.RefObject -} +import useAppStore from '../../data/app_state'; +import SheetHeader from '../ui/SheetHeader'; +import { IMapRoute, IMapServiceInterruption } from 'utils/interfaces'; +import { useServiceInterruptions } from 'app/data/api_query'; +import { SheetProps } from 'app/utils'; // AlertList (for all routes and current route) const AlertList: React.FC = ({ sheetRef }) => { - - const snapPoints = ['25%', '45%', '85%']; - const [snap, _] = useState(1) - - const theme = useAppStore((state) => state.theme); - const selectedRoute = useAppStore((state) => state.selectedRoute); - const setSelectedRoute = useAppStore((state) => state.setSelectedRoute); - const setSelectedRouteDirection = useAppStore((state) => state.setSelectedRouteDirection); - const setOldSelectedRoute = useAppStore((state) => state.setOldSelectedRoute); - const presentSheet = useAppStore((state) => state.presentSheet); - const setSelectedAlert = useAppStore((state) => state.setSelectedAlert); - const dismissSheet = useAppStore((state) => state.dismissSheet); - - const [shownAlerts, setShownAlerts] = useState([]); - - const { data: alerts, isError } = useServiceInterruptions() - - // If no route is selected, we're looking at all routes, therefore show all alerts - // If a route is selected, only show the alerts for that route - useEffect(() => { - if (!alerts) { - setShownAlerts([]); - return - } - - if (!selectedRoute) { - setShownAlerts(alerts); - return; - } - - const alertKeys = selectedRoute.directionList.flatMap(direction => direction.serviceInterruptionKeys); - const filteredAlerts = alerts.filter(alert => alertKeys.includes(Number(alert.key))); - - setShownAlerts(filteredAlerts); - }, [selectedRoute, alerts]); - - const displayDetailAlert = (alert: IMapServiceInterruption) => { - setSelectedAlert(alert); - presentSheet("alertsDetail"); + const snapPoints = ['25%', '45%', '85%']; + const [snap, _] = useState(1); + + const theme = useAppStore((state) => state.theme); + const selectedRoute = useAppStore((state) => state.selectedRoute); + const setSelectedRoute = useAppStore((state) => state.setSelectedRoute); + const setSelectedRouteDirection = useAppStore( + (state) => state.setSelectedRouteDirection, + ); + const setOldSelectedRoute = useAppStore((state) => state.setOldSelectedRoute); + const presentSheet = useAppStore((state) => state.presentSheet); + const setSelectedAlert = useAppStore((state) => state.setSelectedAlert); + const dismissSheet = useAppStore((state) => state.dismissSheet); + + const [shownAlerts, setShownAlerts] = useState([]); + + const { data: alerts, isError } = useServiceInterruptions(); + + // If no route is selected, we're looking at all routes, therefore show all alerts + // If a route is selected, only show the alerts for that route + useEffect(() => { + if (!alerts) { + setShownAlerts([]); + return; } - const handleDismiss = () => { - dismissSheet("alerts") + if (!selectedRoute) { + setShownAlerts(alerts); + return; } - return ( - - - {/* header */} - handleDismiss()}> - - - } - /> - - - - {isError ? - - Error loading alerts. Please try again later. - - : (shownAlerts.length === 0 && - - There are no active alerts at this time. - ) - } - - - - - self.findIndex(o => o.name === obj.name) === index)} - keyExtractor={alert => alert.key} - style={{ height: "100%", marginLeft: 16, paddingTop: 8 }} - contentContainerStyle={{ paddingBottom: 35, paddingRight: 16 }} - renderItem={({ item: alert }) => { - return ( - { - const selectedRouteCopy = selectedRoute as IMapRoute; - setOldSelectedRoute(selectedRouteCopy); // Will be referenced again when dismissing AlertDetail sheet - setSelectedRoute(null); - setSelectedRouteDirection(null); - displayDetailAlert(alert); - }} - style={{ - flexDirection: 'row', - alignItems: 'center', - marginVertical: 4, - backgroundColor: theme.secondaryBackground, - padding: 8, - borderRadius: 8, - }} - > - - {alert.name} - - - ); - }} - /> - - - ) -} - -export default memo(AlertList); \ No newline at end of file + const alertKeys = selectedRoute.directionList.flatMap( + (direction) => direction.serviceInterruptionKeys, + ); + const filteredAlerts = alerts.filter((alert) => + alertKeys.includes(Number(alert.key)), + ); + + setShownAlerts(filteredAlerts); + }, [selectedRoute, alerts]); + + const displayDetailAlert = (alert: IMapServiceInterruption) => { + setSelectedAlert(alert); + presentSheet('alertsDetail'); + }; + + const handleDismiss = () => { + dismissSheet('alerts'); + }; + + return ( + + + {/* header */} + handleDismiss()} + > + + + } + /> + + + + {isError ? ( + + + Error loading alerts. Please try again later. + + + ) : ( + shownAlerts.length === 0 && ( + + + There are no active alerts at this time. + + + ) + )} + + + + self.findIndex((o) => o.name === obj.name) === index, + )} + keyExtractor={(alert) => alert.key} + style={{ height: '100%', marginLeft: 16, paddingTop: 8 }} + contentContainerStyle={{ paddingBottom: 35, paddingRight: 16 }} + renderItem={({ item: alert }) => { + return ( + { + const selectedRouteCopy = selectedRoute as IMapRoute; + setOldSelectedRoute(selectedRouteCopy); // Will be referenced again when dismissing AlertDetail sheet + setSelectedRoute(null); + setSelectedRouteDirection(null); + displayDetailAlert(alert); + }} + style={{ + flexDirection: 'row', + alignItems: 'center', + marginVertical: 4, + backgroundColor: theme.secondaryBackground, + padding: 8, + borderRadius: 8, + }} + > + + {alert.name} + + + ); + }} + /> + + ); +}; + +export default memo(AlertList); diff --git a/app/components/sheets/RouteDetails.tsx b/app/components/sheets/RouteDetails.tsx index 95ad7336..f2f5e17f 100644 --- a/app/components/sheets/RouteDetails.tsx +++ b/app/components/sheets/RouteDetails.tsx @@ -1,235 +1,319 @@ -import React, { useEffect, useState } from "react"; -import { View, Text, TouchableOpacity, NativeSyntheticEvent, Platform } from "react-native"; -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"; -import useAppStore from "../../data/app_state"; -import StopCell from "../ui/StopCell"; -import BusIcon from "../ui/BusIcon"; -import FavoritePill from "../ui/FavoritePill"; -import AlertPill from "../ui/AlertPill"; -import { useQueryClient } from "@tanstack/react-query"; -import { useStopEstimate } from "app/data/api_query"; - -interface SheetProps { - sheetRef: React.RefObject -} +import { + BottomSheetFlatList, + BottomSheetFlatListMethods, + BottomSheetModal, +} from '@gorhom/bottom-sheet'; +import SegmentedControl, { + NativeSegmentedControlIOSChangeEvent, +} from '@react-native-segmented-control/segmented-control'; +import { useQueryClient } from '@tanstack/react-query'; +import { useStopEstimate } from 'app/data/api_query'; +import { SheetProps } from 'app/utils'; +import React, { useEffect, useState } from 'react'; +import { + NativeSyntheticEvent, + Platform, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import { IMapRoute, IPatternPath, IStop } from '../../../utils/interfaces'; +import useAppStore from '../../data/app_state'; +import AlertPill from '../ui/AlertPill'; +import BusIcon from '../ui/BusIcon'; +import FavoritePill from '../ui/FavoritePill'; +import StopCell from '../ui/StopCell'; // 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 selectedRouteDirection = useAppStore(state => state.selectedRouteDirection); - const setSelectedRouteDirection = useAppStore(state => state.setSelectedRouteDirection); - const setSelectedStop = useAppStore(state => state.setSelectedStop); - const setPoppedUpStopCallout = useAppStore(state => state.setPoppedUpStopCallout); - const setSheetCloseCallback = useAppStore(state => state.setSheetCloseCallback); - const setScrollToStop = useAppStore(state => state.setScrollToStop); - const dismissSheet = useAppStore((state) => state.dismissSheet); - const theme = useAppStore(state => state.theme); - - const { data: stopEstimates } = useStopEstimate( - currentSelectedRoute?.key ?? "", - currentSelectedRoute?.directionList[0]?.direction.key ?? "", - currentSelectedRoute?.patternPaths[0]?.patternPoints[0]?.stop?.stopCode ?? "" - ) - - - // Controls SegmentedControl - const [selectedDirectionIndex, setSelectedDirectionIndex] = useState(0); - - const [processedStops, setProcessedStops] = useState([]); - const [selectedRoute, setSelectedRoute] = useState(null); - - const client = useQueryClient(); - - // Filters patternPaths for only the selected route from all patternPaths - function getPatternPathForSelectedRoute(): IPatternPath | undefined { - if (!selectedRoute) return undefined; - return selectedRoute.patternPaths.find(direction => direction.patternKey === selectedRoute.directionList[selectedDirectionIndex]?.patternList[0]?.key) - } - - const handleDismiss = () => { - dismissSheet("routeDetails"); + const flatListRef = React.useRef(null); + + const currentSelectedRoute = useAppStore((state) => state.selectedRoute); + const clearSelectedRoute = useAppStore((state) => state.clearSelectedRoute); + + const [futurePosition, setFuturePosition] = useState(-1); + + const selectedRouteDirection = useAppStore( + (state) => state.selectedRouteDirection, + ); + const setSelectedRouteDirection = useAppStore( + (state) => state.setSelectedRouteDirection, + ); + const setSelectedStop = useAppStore((state) => state.setSelectedStop); + const setPoppedUpStopCallout = useAppStore( + (state) => state.setPoppedUpStopCallout, + ); + const setSheetCloseCallback = useAppStore( + (state) => state.setSheetCloseCallback, + ); + const setScrollToStop = useAppStore((state) => state.setScrollToStop); + const dismissSheet = useAppStore((state) => state.dismissSheet); + const theme = useAppStore((state) => state.theme); + + const { data: stopEstimates } = useStopEstimate( + currentSelectedRoute?.key ?? '', + currentSelectedRoute?.directionList[0]?.direction.key ?? '', + currentSelectedRoute?.patternPaths[0]?.patternPoints[0]?.stop?.stopCode ?? + '', + ); + + // Controls SegmentedControl + const [selectedDirectionIndex, setSelectedDirectionIndex] = useState(0); + + const [processedStops, setProcessedStops] = useState([]); + const [selectedRoute, setSelectedRoute] = useState(null); + + const client = useQueryClient(); + + // Filters patternPaths for only the selected route from all patternPaths + function getPatternPathForSelectedRoute(): IPatternPath | undefined { + if (!selectedRoute) return undefined; + return selectedRoute.patternPaths.find( + (direction) => + direction.patternKey === + selectedRoute.directionList[selectedDirectionIndex]?.patternList[0] + ?.key, + ); + } + + const handleDismiss = () => { + dismissSheet('routeDetails'); + }; + + // When a new route is selected or the direction of the route is changed, update the stops + useEffect(() => { + if (!selectedRoute) return; + + const processedStops: IStop[] = []; + const directionPath = getPatternPathForSelectedRoute()?.patternPoints ?? []; + + for (const point of directionPath) { + if (!point.stop) continue; + processedStops.push(point.stop); } - // When a new route is selected or the direction of the route is changed, update the stops - useEffect(() => { - if (!selectedRoute) return; - - const processedStops: IStop[] = []; - const directionPath = getPatternPathForSelectedRoute()?.patternPoints ?? []; - - for (const point of directionPath) { - if (!point.stop) continue; - processedStops.push(point.stop); + setProcessedStops(processedStops); + }, [selectedRoute, selectedDirectionIndex]); + + // Update the selected route when the currentSelectedRoute changes but only if it is not null + // Prevents visual glitch when the sheet is closed and the selected route is null + useEffect(() => { + if (!currentSelectedRoute) return; + setSelectedRoute(currentSelectedRoute); + + // reset direction selector + setSelectedRouteDirection( + currentSelectedRoute.directionList[0]?.direction.key ?? null, + ); + setSelectedDirectionIndex(0); + }, [currentSelectedRoute]); + + // update the segmented control when the selected direction changes + useEffect(() => { + if (!selectedRoute) return; + + const directionIndex = selectedRoute.directionList.findIndex( + (direction) => direction.direction.key === selectedRouteDirection, + ); + + if (directionIndex === -1) return; + + setSelectedDirectionIndex(directionIndex); + }, [selectedRouteDirection]); + + useEffect(() => { + setScrollToStop((stop) => { + const index = getPatternPathForSelectedRoute() + ?.patternPoints.filter((st) => st.stop) + .findIndex((st) => st.stop && st.stop?.stopCode === stop.stopCode); + + if (index && index !== -1) { + sheetRef.current?.snapToIndex(2); + setFuturePosition(index); + } + }); + }, [selectedRoute, selectedRouteDirection]); + + useEffect(() => { + setSheetCloseCallback(() => { + clearSelectedRoute(); + setSelectedRouteDirection(null); + + setSelectedStop(null); + setPoppedUpStopCallout(null); + + // reset direction selector + setSelectedDirectionIndex(0); + }, 'routeDetails'); + + return () => setSelectedRouteDirection(null); + }, []); + + const handleSetSelectedDirection = ( + evt: NativeSyntheticEvent, + ) => { + setSelectedDirectionIndex(evt.nativeEvent.selectedSegmentIndex); + + setSelectedRouteDirection( + selectedRoute?.directionList[evt.nativeEvent.selectedSegmentIndex] + ?.direction.key ?? null, + ); + }; + + const snapPoints = ['25%', '45%', '85%']; + const [snap, _] = useState(1); + + return ( + { + if (futurePosition !== -1) { + flatListRef.current?.scrollToIndex({ + index: futurePosition, + animated: true, + }); + setFuturePosition(-1); } - - setProcessedStops(processedStops); - }, [selectedRoute, selectedDirectionIndex]) - - // Update the selected route when the currentSelectedRoute changes but only if it is not null - // Prevents visual glitch when the sheet is closed and the selected route is null - useEffect(() => { - if (!currentSelectedRoute) return; - setSelectedRoute(currentSelectedRoute); - - // reset direction selector - setSelectedRouteDirection(currentSelectedRoute.directionList[0]?.direction.key ?? null); - setSelectedDirectionIndex(0); - - }, [currentSelectedRoute]) - - - // update the segmented control when the selected direction changes - useEffect(() => { - if (!selectedRoute) return; - - const directionIndex = selectedRoute.directionList.findIndex(direction => direction.direction.key === selectedRouteDirection); - - if (directionIndex === -1) return; - - setSelectedDirectionIndex(directionIndex); - }, [selectedRouteDirection]); - - useEffect(() => { - setScrollToStop((stop) => { - const index = getPatternPathForSelectedRoute() - ?.patternPoints - .filter(st => st.stop) - .findIndex(st => st.stop && st.stop?.stopCode === stop.stopCode); - - if (index && index != -1) { - sheetRef.current?.snapToIndex(2); - setFuturePosition(index); - } - }) - }, [selectedRoute, selectedRouteDirection]) - - useEffect(() => { - setSheetCloseCallback(() => { - clearSelectedRoute(); - setSelectedRouteDirection(null); - - setSelectedStop(null); - setPoppedUpStopCallout(null); - - // reset direction selector - setSelectedDirectionIndex(0); - }, "routeDetails") - - return () => setSelectedRouteDirection(null); - }, []); - - const handleSetSelectedDirection = (evt: NativeSyntheticEvent) => { - setSelectedDirectionIndex(evt.nativeEvent.selectedSegmentIndex); - - setSelectedRouteDirection(selectedRoute?.directionList[evt.nativeEvent.selectedSegmentIndex]?.direction.key ?? null); - } - - const snapPoints = ['25%', '45%', '85%']; - const [snap, _] = useState(1) - - - return ( - { - if (futurePosition !== -1) { - flatListRef.current?.scrollToIndex({ index: futurePosition, animated: true }); - setFuturePosition(-1); - } + }} + > + {selectedRoute && ( + + - {selectedRoute && - - - - {selectedRoute?.name ?? "Something went wrong"} - - - - - - - - - - - - { selectedRoute?.directionList.length > 1 && - "to " + direction.destination) ?? []} - selectedIndex={selectedDirectionIndex} - onChange={handleSetSelectedDirection} - backgroundColor={Platform.OS == "android" ? theme.androidSegmentedBackground : undefined} - /> - } - - - - } - - - { selectedRoute && - client.invalidateQueries({ queryKey: ["stopEstimate"] })} - refreshing={false} - ItemSeparatorComponent={() => } - renderItem={({ item: stop, index }) => { - - // handle the last cell showing No upcoming departures - var direction; - if (index == processedStops.length-1 && selectedRoute?.directionList.length > 1) { - direction = selectedRoute?.directionList[selectedDirectionIndex == 0 ? 1 : 0]?.direction! - } else { - direction = selectedRoute?.directionList[selectedDirectionIndex]?.direction! - } - - return ( - sheetRef.current?.snapToIndex(pos)} - /> - ); - }} - /> + > + + + {selectedRoute?.name ?? 'Something went wrong'} + + + + + + + + + + + + + {selectedRoute?.directionList.length > 1 && ( + 'to ' + direction.destination, + ) ?? [] + } + selectedIndex={selectedDirectionIndex} + onChange={handleSetSelectedDirection} + backgroundColor={ + Platform.OS === 'android' + ? theme.androidSegmentedBackground + : undefined + } + /> + )} + + + + )} + + {selectedRoute && ( + + client.invalidateQueries({ queryKey: ['stopEstimate'] }) + } + refreshing={false} + ItemSeparatorComponent={() => ( + + )} + renderItem={({ item: stop, index }) => { + // handle the last cell showing No upcoming departures + let direction; + if ( + index === processedStops.length - 1 && + selectedRoute?.directionList.length > 1 + ) { + direction = + selectedRoute?.directionList[ + selectedDirectionIndex === 0 ? 1 : 0 + ]?.direction!; + } else { + direction = + selectedRoute?.directionList[selectedDirectionIndex] + ?.direction!; } - {!selectedRoute && ( - - Something went wrong. - - )} - - ) -} - + return ( + sheetRef.current?.snapToIndex(pos)} + /> + ); + }} + /> + )} + + {!selectedRoute && ( + + Something went wrong. + + )} + + ); +}; export default RouteDetails; diff --git a/app/components/sheets/RoutesList.tsx b/app/components/sheets/RoutesList.tsx index f4376ef9..4b137b66 100644 --- a/app/components/sheets/RoutesList.tsx +++ b/app/components/sheets/RoutesList.tsx @@ -1,212 +1,315 @@ -import React, { memo, useEffect, useState } from "react"; -import { ActivityIndicator, View, TouchableOpacity, Text, NativeSyntheticEvent, Platform } from "react-native"; -import SegmentedControl, { NativeSegmentedControlIOSChangeEvent } from "@react-native-segmented-control/segmented-control"; -import { BottomSheetModal, BottomSheetView, BottomSheetFlatList } from "@gorhom/bottom-sheet"; import { FontAwesome, FontAwesome6, MaterialIcons } from '@expo/vector-icons'; - -import { IDirectionList, IMapRoute } from "../../../utils/interfaces"; -import useAppStore from "../../data/app_state"; -import BusIcon from "../ui/BusIcon"; -import SheetHeader from "../ui/SheetHeader"; -import IconPill from "../ui/IconPill"; -import { useAuthToken, useBaseData, usePatternPaths, useRoutes } from "app/data/api_query"; -import { useDefaultRouteGroup, useFavorites } from "app/data/storage_query"; -import { useQueryClient } from "@tanstack/react-query"; - -interface SheetProps { - sheetRef: React.RefObject -} +import { BottomSheetFlatList, BottomSheetModal } from '@gorhom/bottom-sheet'; +import SegmentedControl, { + NativeSegmentedControlIOSChangeEvent, +} from '@react-native-segmented-control/segmented-control'; +import { useQueryClient } from '@tanstack/react-query'; +import { useRoutes } from 'app/data/api_query'; +import { useDefaultRouteGroup, useFavorites } from 'app/data/storage_query'; +import { SheetProps } from 'app/utils'; +import React, { memo, useEffect, useState } from 'react'; +import { + ActivityIndicator, + NativeSyntheticEvent, + Platform, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import { IDirectionList, IMapRoute } from '../../../utils/interfaces'; +import useAppStore from '../../data/app_state'; +import BusIcon from '../ui/BusIcon'; +import IconPill from '../ui/IconPill'; +import SheetHeader from '../ui/SheetHeader'; // Display routes list for all routes and favorite routes const RoutesList: React.FC = ({ sheetRef }) => { - const snapPoints = ['25%', '45%', '85%']; - const [snap, setSnap] = useState(1); - - const setSelectedRoute = useAppStore((state) => state.setSelectedRoute); - const selectedRouteCategory = useAppStore(state => state.selectedRouteCategory); - const setSelectedRouteCategory = useAppStore(state => state.setSelectedRouteCategory); - const setDrawnRoutes = useAppStore((state) => state.setDrawnRoutes); - const presentSheet = useAppStore((state) => state.presentSheet); - const theme = useAppStore((state) => state.theme); - - const queryClient = useQueryClient(); - const { data: routes, isLoading: isRoutesLoading, isRefetching: isRefreshing, refetch: refetchRoutes } = useRoutes(); - const { data: favorites, isLoading: isFavoritesLoading, isError: isFavoritesError, refetch: refetchFavorites } = useFavorites(); - const { data: defaultGroup, refetch: refetchDefaultGroup } = useDefaultRouteGroup(); - - const routeError = [useRoutes().isError, useAuthToken().isError, usePatternPaths().isError, useBaseData().isError].some((v) => v == true); - - const handleRouteSelected = (selectedRoute: IMapRoute) => { - setSelectedRoute(selectedRoute); - setDrawnRoutes([selectedRoute]); - presentSheet("routeDetails"); - } + const snapPoints = ['25%', '45%', '85%']; + const [snap, setSnap] = useState(1); - function filterRoutes(): IMapRoute[] { - if (!routes) return []; - - switch(selectedRouteCategory) { - case "All Routes": - return routes; - case "Gameday": - return routes.filter((route) => route.name.includes("Gameday")) - case "Favorites": - return favorites ?? [] - } - } + const setSelectedRoute = useAppStore((state) => state.setSelectedRoute); + const selectedRouteCategory = useAppStore( + (state) => state.selectedRouteCategory, + ); + const setSelectedRouteCategory = useAppStore( + (state) => state.setSelectedRouteCategory, + ); + const setDrawnRoutes = useAppStore((state) => state.setDrawnRoutes); + const presentSheet = useAppStore((state) => state.presentSheet); + const theme = useAppStore((state) => state.theme); + + const queryClient = useQueryClient(); + const { + data: routes, + isLoading: isRoutesLoading, + isError: routeError, + isRefetching: isRefreshing, + refetch: refetchRoutes, + } = useRoutes(); + const { + data: favorites, + isLoading: isFavoritesLoading, + isError: isFavoritesError, + refetch: refetchFavorites, + } = useFavorites(); + const { data: defaultGroup, refetch: refetchDefaultGroup } = + useDefaultRouteGroup(); - useEffect(() => { - setSelectedRouteCategory(defaultGroup === 0 ? "All Routes" : "Favorites"); - }, [defaultGroup]); - - useEffect(() => { - refetchRoutes(); - }, [theme]) - - // Update the shown routes when the selectedRouteCategory changes - useEffect(() => { - const filteredRoutes = filterRoutes(); - setDrawnRoutes(filteredRoutes); - }, [selectedRouteCategory, routes, favorites]); - - // Update the favorites when the view is focused - function onAnimate(from: number, to: number) { - setSnap(to) - if (from==-1) { - refetchDefaultGroup() - refetchFavorites() - - setDrawnRoutes(filterRoutes()); - } + const handleRouteSelected = (selectedRoute: IMapRoute) => { + setSelectedRoute(selectedRoute); + setDrawnRoutes([selectedRoute]); + presentSheet('routeDetails'); + }; + + function filterRoutes(): IMapRoute[] { + if (!routes) return []; + + switch (selectedRouteCategory) { + case 'All Routes': + return routes; + case 'Gameday': + return routes.filter((route) => route.name.includes('Gameday')); + case 'Favorites': + return favorites ?? []; } + } + + useEffect(() => { + setSelectedRouteCategory(defaultGroup === 0 ? 'All Routes' : 'Favorites'); + }, [defaultGroup]); + + useEffect(() => { + refetchRoutes(); + }, [theme]); - function getRouteCategories(): Array<"All Routes" | "Gameday" | "Favorites"> { - // if gameday routes are available - if (routes && routes.some((element) => element.name.includes("Gameday"))) { - return ["All Routes", "Gameday", "Favorites"] - } + // Update the shown routes when the selectedRouteCategory changes + useEffect(() => { + const filteredRoutes = filterRoutes(); + setDrawnRoutes(filteredRoutes); + }, [selectedRouteCategory, routes, favorites]); - return ["All Routes", "Favorites"] + // Update the favorites when the view is focused + function onAnimate(from: number, to: number) { + setSnap(to); + if (from === -1) { + refetchDefaultGroup(); + refetchFavorites(); + + setDrawnRoutes(filterRoutes()); } + } - function androidHandleDismss(to: number) { - if (to != -1) { - refetchDefaultGroup() - refetchFavorites() - - setDrawnRoutes(filterRoutes()); - } + function getRouteCategories(): ('All Routes' | 'Gameday' | 'Favorites')[] { + // if gameday routes are available + if (routes && routes.some((element) => element.name.includes('Gameday'))) { + return ['All Routes', 'Gameday', 'Favorites']; } - const handleSetSelectedRouteCategory = (evt: NativeSyntheticEvent) => { - setSelectedRouteCategory(getRouteCategories()[evt.nativeEvent.selectedSegmentIndex] ?? "All Routes") + return ['All Routes', 'Favorites']; + } + + function androidHandleDismss(to: number) { + if (to !== -1) { + refetchDefaultGroup(); + refetchFavorites(); + + setDrawnRoutes(filterRoutes()); } + } + + const handleSetSelectedRouteCategory = ( + evt: NativeSyntheticEvent, + ) => { + setSelectedRouteCategory( + getRouteCategories()[evt.nativeEvent.selectedSegmentIndex] ?? + 'All Routes', + ); + }; - return ( - - - - {/* Route Planning */} - presentSheet("inputRoute")} > - } - text="Plan Route" - /> - - - {/* Settings */} - presentSheet("settings")}> - } - /> - - } + return ( + + + + {/* Route Planning */} + presentSheet('inputRoute')}> + + } + text="Plan Route" /> + - presentSheet('settings')} + > + + } /> - + + + } + /> + + + + + {!isFavoritesLoading && + selectedRouteCategory === 'Favorites' && + favorites?.length === 0 && + routes?.length !== 0 && ( + + + You don't have any favorite routes. + + + )} + + {/* Loading indicatior, only show if no error and either loading or there are no routes */} + {!routeError && (isRoutesLoading || !routes) && ( + + )} + + {/* Error */} + {routeError ? ( + + + Error loading routes. Please try again later. + + + ) : ( + isFavoritesError && + selectedRouteCategory === 'Favorites' && ( + + + Error loading favorites. Please try again later. + + + ) + )} + - { (!isFavoritesLoading) && selectedRouteCategory === "Favorites" && favorites?.length === 0 && routes?.length != 0 && ( - - You don't have any favorite routes. - + route.key} + refreshing={isRefreshing} + onRefresh={() => { + queryClient.invalidateQueries({ queryKey: ['baseData'] }); + queryClient.invalidateQueries({ queryKey: ['patternPaths'] }); + queryClient.invalidateQueries({ queryKey: ['routes'] }); + }} + renderItem={({ item: route }) => { + return ( + handleRouteSelected(route)} + > + + + + + {route.name} + + {favorites?.some( + (fav) => fav.shortName === route.shortName, + ) && ( + + )} + + {route.directionList.length > 1 ? ( + + {route.directionList.map( + (elm: IDirectionList, index: number) => ( + + + {elm.destination} + + {index !== route.directionList.length - 1 && ( + + | + + )} + + ), + )} + + ) : ( + Campus Circulator )} + + + ); + }} + /> + + ); +}; - {/* Loading indicatior, only show if no error and either loading or there are no routes */} - { (!routeError && (isRoutesLoading || !routes)) && } - - {/* Error */} - { routeError ? - - Error loading routes. Please try again later. - - : (isFavoritesError && selectedRouteCategory === "Favorites") && - - Error loading favorites. Please try again later. - - } - - - route.key} - refreshing={isRefreshing} - onRefresh={() => { - queryClient.invalidateQueries({ queryKey: ["baseData"] }); - queryClient.invalidateQueries({ queryKey: ["patternPaths"] }); - queryClient.invalidateQueries({ queryKey: ["routes"] }); - }} - renderItem={({item: route}) => { - return ( - handleRouteSelected(route)}> - - - - {route.name} - {favorites?.some((fav) => fav.shortName == route.shortName) && - - } - - { route.directionList.length > 1 ? - - {route.directionList.map((elm: IDirectionList, index: number) => ( - - {elm.destination} - {index !== route.directionList.length - 1 && |} - - ))} - - : - Campus Circulator - } - - - ) - }} - /> - - - ) -} - -export default memo(RoutesList) \ No newline at end of file +export default memo(RoutesList); diff --git a/app/components/sheets/Settings.tsx b/app/components/sheets/Settings.tsx index 4b326d8f..9ee2f068 100644 --- a/app/components/sheets/Settings.tsx +++ b/app/components/sheets/Settings.tsx @@ -1,128 +1,167 @@ -import React, { memo, useEffect, useState } from "react"; -import { View, Text, TouchableOpacity, NativeSyntheticEvent, Platform, Appearance } from "react-native"; -import { BottomSheetModal, BottomSheetView, BottomSheetScrollView } from "@gorhom/bottom-sheet"; +import React, { memo, useEffect, useState } from 'react'; +import { + View, + Text, + TouchableOpacity, + NativeSyntheticEvent, + Platform, + Appearance, +} from 'react-native'; +import { BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet'; import Ionicons from '@expo/vector-icons/Ionicons'; -import SheetHeader from "../ui/SheetHeader"; -import SegmentedControl, { NativeSegmentedControlIOSChangeEvent } from "@react-native-segmented-control/segmented-control"; -import AsyncStorage from "@react-native-async-storage/async-storage"; -import useAppStore from "../../data/app_state"; -import { defaultGroupMutation, useDefaultRouteGroup } from "app/data/storage_query"; -import { getColorScheme } from "app/utils"; -import { darkMode, lightMode } from "app/theme"; -import { useQueryClient } from "@tanstack/react-query"; - -interface SheetProps { - sheetRef: React.RefObject -} +import SheetHeader from '../ui/SheetHeader'; +import SegmentedControl, { + NativeSegmentedControlIOSChangeEvent, +} from '@react-native-segmented-control/segmented-control'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import useAppStore from '../../data/app_state'; +import { + defaultGroupMutation, + useDefaultRouteGroup, +} from 'app/data/storage_query'; +import { getColorScheme, SheetProps } from 'app/utils'; +import { darkMode, lightMode } from 'app/theme'; +import { useQueryClient } from '@tanstack/react-query'; // Settings (for all routes and current route) const Settings: React.FC = ({ sheetRef }) => { - const snapPoints = ['25%', '45%', '85%']; - const [snap, _] = useState(1) - - const [themeSetting, setTheme] = useState(0); - const [defaultGroupSetting, setDefaultGroupState] = useState(0); - - const theme = useAppStore((state) => state.theme); - const setAppTheme = useAppStore((state) => state.setTheme); - const dismissSheet = useAppStore((state) => state.dismissSheet); - - const { data: defaultGroup, refetch: refetchDefaultGroup } = useDefaultRouteGroup(); - const setDefaultGroup = defaultGroupMutation(); - const client = useQueryClient(); - - function setDefaultGroupValue(evt: NativeSyntheticEvent) { - setDefaultGroup.mutate(evt.nativeEvent.selectedSegmentIndex); - setDefaultGroupState(evt.nativeEvent.selectedSegmentIndex); - refetchDefaultGroup() + const snapPoints = ['25%', '45%', '85%']; + const [snap, _] = useState(1); + + const [themeSetting, setTheme] = useState(0); + const [defaultGroupSetting, setDefaultGroupState] = useState(0); + + const theme = useAppStore((state) => state.theme); + const setAppTheme = useAppStore((state) => state.setTheme); + const dismissSheet = useAppStore((state) => state.dismissSheet); + + const { data: defaultGroup, refetch: refetchDefaultGroup } = + useDefaultRouteGroup(); + const setDefaultGroup = defaultGroupMutation(); + const client = useQueryClient(); + + function setDefaultGroupValue( + evt: NativeSyntheticEvent, + ) { + setDefaultGroupState(evt.nativeEvent.selectedSegmentIndex); + setDefaultGroup.mutate(evt.nativeEvent.selectedSegmentIndex, { + onSuccess: () => { + refetchDefaultGroup(); + }, + }); + } + + function setAppThemeValue( + evt: NativeSyntheticEvent, + ) { + setTheme(evt.nativeEvent.selectedSegmentIndex); + AsyncStorage.setItem( + 'app-theme', + evt.nativeEvent.selectedSegmentIndex.toString(), + ); + + getColorScheme().then((newTheme) => { + const t = newTheme === 'dark' ? darkMode : lightMode; + + setAppTheme(t); + Appearance.setColorScheme(t.mode); + + client.invalidateQueries({ queryKey: ['routes'] }); + client.refetchQueries({ queryKey: ['routes'] }); + }); + } + + useEffect(() => { + AsyncStorage.getItem('app-theme').then((value) => { + if (value) { + setTheme(Number(value)); + } + const systemTheme = Appearance.getColorScheme() ?? 'light'; + AsyncStorage.setItem('system-theme', systemTheme); + }); + }, []); + + useEffect(() => { + if (defaultGroup) { + setDefaultGroupState(defaultGroup); } - - function setAppThemeValue(evt: NativeSyntheticEvent) { - setTheme(evt.nativeEvent.selectedSegmentIndex); - AsyncStorage.setItem('app-theme', evt.nativeEvent.selectedSegmentIndex.toString()); - - getColorScheme().then((newTheme) => { - const t = newTheme == "dark" ? darkMode : lightMode - - setAppTheme(t); - Appearance.setColorScheme(t.mode); - - client.invalidateQueries({ queryKey: ["routes"] }) - client.refetchQueries({ queryKey: ["routes"] }) - }) - } - - useEffect(() => { - AsyncStorage.getItem('app-theme').then((value) => { - if (value) { - setTheme(Number(value)); - } - const systemTheme = Appearance.getColorScheme() ?? "light" - AsyncStorage.setItem('system-theme', systemTheme) - }) - }, []) - - useEffect(() => { - if (defaultGroup) { - setDefaultGroupState(defaultGroup); - } - }, [defaultGroup]) - - return ( - - - {/* header */} - dismissSheet("settings")}> - - - } - /> - - - - - - - + + {/* header */} + dismissSheet('settings')} > - - Default Route Group - Choose the default route group to display when the app opens - - - - - App Theme - Choose the theme that the app uses. - - - - - - ) -} + + + } + /> + + + + + + + Default Route Group + + + Choose the default route group to display when the app opens + + + + + + + App Theme + + + Choose the theme that the app uses. + + + + + + ); +}; export default memo(Settings); diff --git a/app/components/sheets/StopTimetable.tsx b/app/components/sheets/StopTimetable.tsx index 735e99ff..42521786 100644 --- a/app/components/sheets/StopTimetable.tsx +++ b/app/components/sheets/StopTimetable.tsx @@ -1,221 +1,307 @@ -import { useEffect, useState } from "react"; -import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native"; -import { BottomSheetModal, BottomSheetView, BottomSheetScrollView } from "@gorhom/bottom-sheet"; -import { FlatList } from "react-native-gesture-handler"; -import { Ionicons } from "@expo/vector-icons"; -import useAppStore from "../../data/app_state"; -import { IRouteStopSchedule, IStop } from "../../../utils/interfaces"; -import Timetable from "../ui/Timetable"; -import moment from "moment-strftime"; +import { useEffect, useState } from 'react'; +import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native'; +import { BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet'; +import { FlatList } from 'react-native-gesture-handler'; +import { Ionicons } from '@expo/vector-icons'; +import useAppStore from '../../data/app_state'; +import { IRouteStopSchedule, IStop } from '../../../utils/interfaces'; +import Timetable from '../ui/Timetable'; +import moment from 'moment-strftime'; import DateSelector from '../ui/DateSelector'; -import SheetHeader from "../ui/SheetHeader"; -import { useRoutes, useSchedule } from "app/data/api_query"; - -interface SheetProps { - sheetRef: React.RefObject -} +import SheetHeader from '../ui/SheetHeader'; +import { useRoutes, useSchedule } from 'app/data/api_query'; +import { SheetProps } from 'app/utils'; // Timtable with upcoming routes const StopTimetable: React.FC = ({ sheetRef }) => { - const selectedStop = useAppStore((state) => state.selectedStop); - const setSelectedStop = useAppStore((state) => state.setSelectedStop); - - const selectedRoute = useAppStore((state) => state.selectedRoute); - const setSelectedRoute = useAppStore((state) => state.setSelectedRoute); - const setDrawnRoutes = useAppStore((state) => state.setDrawnRoutes); - const presentSheet = useAppStore((state) => state.presentSheet); - const dismissSheet = useAppStore((state) => state.dismissSheet); - const setSheetCloseCallback = useAppStore((state) => state.setSheetCloseCallback); - - const selectedTimetableDate = useAppStore((state) => state.selectedTimetableDate); - const setSelectedTimetableDate = useAppStore((state) => state.setSelectedTimetableDate); - - const [tempSelectedStop, setTempSelectedStop] = useState(null); - const [showNonRouteSchedules, setShowNonRouteSchedules] = useState(false); - const [nonRouteSchedules, setNonRouteSchedules] = useState(null); - const [routeSchedules, setRouteSchedules] = useState(null); - const theme = useAppStore((state) => state.theme); - - const { data: routes } = useRoutes(); - const { - data: stopSchedule, - isError: scheduleError, - isLoading: scheduleLoading - } = useSchedule(selectedStop?.stopCode ?? "", selectedTimetableDate ?? moment().toDate()); - - const dayDecrement = () => { - // Decrease the date by one day - const prevDate = moment(selectedTimetableDate || moment().toDate()).subtract(1, 'days').toDate(); - setRouteSchedules(null); - setNonRouteSchedules(null); - setSelectedTimetableDate(prevDate); - }; - - const dayIncrement = () => { - // Increase the date by one day - const nextDate = moment(selectedTimetableDate || moment().toDate()).add(1, 'days').toDate(); - setRouteSchedules(null); - setNonRouteSchedules(null); - setSelectedTimetableDate(nextDate); - }; - - useEffect(() => { - if (!stopSchedule) return; - - // find the schedules for the selected route - let routeStops = stopSchedule.routeStopSchedules.filter((schedule) => schedule.routeName === selectedRoute?.name && schedule.routeNumber === selectedRoute?.shortName) - - // filter anything that is end of route - routeStops = routeStops.filter((schedule) => !schedule.isEndOfRoute); - setRouteSchedules(routeStops); - - // filter out non route schedules - let nonRouteStops = stopSchedule.routeStopSchedules.filter((schedule) => schedule.routeName !== selectedRoute?.name || schedule.routeNumber !== selectedRoute?.shortName) - - // filter anything that doesnt have stop times - nonRouteStops = nonRouteStops.filter((schedule) => schedule.stopTimes.length > 0); - setNonRouteSchedules(nonRouteStops) - - }, [stopSchedule]); - - function getLineColor(shortName: string) { - const route = routes?.find((route) => route.shortName === shortName); - return route?.directionList[0]?.lineColor ?? "#500000"; - } - - // prevent data from disappearing when the sheet is closed - useEffect(() => { - if (!selectedStop) return; - - if (!selectedTimetableDate) setSelectedTimetableDate(moment().toDate()); - - setTempSelectedStop(selectedStop); - }, [selectedStop, selectedTimetableDate]) - - useEffect(() => { - setSheetCloseCallback(() => { - setRouteSchedules(null); - setNonRouteSchedules(null); - setSelectedStop(null); - setShowNonRouteSchedules(false); - setSelectedTimetableDate(null); - }, "stopTimetable") - }, []) - - - const snapPoints = ['25%', '45%', '85%']; - const [snap, _] = useState(2) - - return ( - state.selectedStop); + const setSelectedStop = useAppStore((state) => state.setSelectedStop); + + const selectedRoute = useAppStore((state) => state.selectedRoute); + const setSelectedRoute = useAppStore((state) => state.setSelectedRoute); + const setDrawnRoutes = useAppStore((state) => state.setDrawnRoutes); + const presentSheet = useAppStore((state) => state.presentSheet); + const dismissSheet = useAppStore((state) => state.dismissSheet); + const setSheetCloseCallback = useAppStore( + (state) => state.setSheetCloseCallback, + ); + + const selectedTimetableDate = useAppStore( + (state) => state.selectedTimetableDate, + ); + const setSelectedTimetableDate = useAppStore( + (state) => state.setSelectedTimetableDate, + ); + + const [tempSelectedStop, setTempSelectedStop] = useState(null); + const [showNonRouteSchedules, setShowNonRouteSchedules] = + useState(false); + const [nonRouteSchedules, setNonRouteSchedules] = useState< + IRouteStopSchedule[] | null + >(null); + const [routeSchedules, setRouteSchedules] = useState< + IRouteStopSchedule[] | null + >(null); + const theme = useAppStore((state) => state.theme); + + const { data: routes } = useRoutes(); + const { + data: stopSchedule, + isError: scheduleError, + isLoading: scheduleLoading, + } = useSchedule( + selectedStop?.stopCode ?? '', + selectedTimetableDate ?? moment().toDate(), + ); + + const dayDecrement = () => { + // Decrease the date by one day + const prevDate = moment(selectedTimetableDate || moment().toDate()) + .subtract(1, 'days') + .toDate(); + setRouteSchedules(null); + setNonRouteSchedules(null); + setSelectedTimetableDate(prevDate); + }; + + const dayIncrement = () => { + // Increase the date by one day + const nextDate = moment(selectedTimetableDate || moment().toDate()) + .add(1, 'days') + .toDate(); + setRouteSchedules(null); + setNonRouteSchedules(null); + setSelectedTimetableDate(nextDate); + }; + + useEffect(() => { + if (!stopSchedule) return; + + // find the schedules for the selected route + let routeStops = stopSchedule.routeStopSchedules.filter( + (schedule) => + schedule.routeName === selectedRoute?.name && + schedule.routeNumber === selectedRoute?.shortName, + ); + + // filter anything that is end of route + routeStops = routeStops.filter((schedule) => !schedule.isEndOfRoute); + setRouteSchedules(routeStops); + + // filter out non route schedules + let nonRouteStops = stopSchedule.routeStopSchedules.filter( + (schedule) => + schedule.routeName !== selectedRoute?.name || + schedule.routeNumber !== selectedRoute?.shortName, + ); + + // filter anything that doesnt have stop times + nonRouteStops = nonRouteStops.filter( + (schedule) => schedule.stopTimes.length > 0, + ); + setNonRouteSchedules(nonRouteStops); + }, [stopSchedule]); + + function getLineColor(shortName: string) { + const route = routes?.find((route) => route.shortName === shortName); + return route?.directionList[0]?.lineColor ?? '#500000'; + } + + // prevent data from disappearing when the sheet is closed + useEffect(() => { + if (!selectedStop) return; + + if (!selectedTimetableDate) setSelectedTimetableDate(moment().toDate()); + + setTempSelectedStop(selectedStop); + }, [selectedStop, selectedTimetableDate]); + + useEffect(() => { + setSheetCloseCallback(() => { + setRouteSchedules(null); + setNonRouteSchedules(null); + setSelectedStop(null); + setShowNonRouteSchedules(false); + setSelectedTimetableDate(null); + }, 'stopTimetable'); + }, []); + + const snapPoints = ['25%', '45%', '85%']; + const [snap, _] = useState(2); + + return ( + + + dismissSheet('stopTimetable')} + > + + + } + /> + + + + {scheduleError && ( + + Unable to load schedules. Please try again later + + )} + + {!scheduleError && ( + - - dismissSheet("stopTimetable")}> - - - } + + + + + + + {scheduleLoading && } + + {routeSchedules && ( + index.toString()} + ItemSeparatorComponent={() => ( + - - - - - { scheduleError && Unable to load schedules. Please try again later } - - {!scheduleError && ( - - - - - + )} + style={{ marginRight: 16 }} + renderItem={({ item, index }) => { + return ( + + + + ); + }} + /> + )} + + {showNonRouteSchedules && ( + + + index.toString()} + scrollEnabled={false} + style={{ marginRight: 16 }} + ItemSeparatorComponent={() => ( + + )} + renderItem={({ item, index }) => { + return ( + + { + const route = routes!.find( + (route) => route.shortName === item.routeNumber, + ); + + if (route) { + dismissSheet('stopTimetable'); + + setSelectedRoute(route); + setDrawnRoutes([route]); + presentSheet('routeDetails'); + } + }} + tintColor={getLineColor(item.routeNumber)} + stopCode={selectedStop?.stopCode ?? ''} + /> + ); + }} + /> + + )} + + {nonRouteSchedules && nonRouteSchedules.length > 0 && ( + // show other routes button + setShowNonRouteSchedules(!showNonRouteSchedules)} + > + + {showNonRouteSchedules ? 'Hide' : 'Show'} Other Routes + + + )} + + )} + + ); +}; - {scheduleLoading && } - - {routeSchedules && ( - index.toString()} - ItemSeparatorComponent={() => } - style={{marginRight: 16}} - renderItem={({ item, index }) => { - return ( - - - - ); - }} - /> - )} - - {showNonRouteSchedules && ( - - - index.toString()} - scrollEnabled={false} - style={{marginRight: 16}} - ItemSeparatorComponent={() => } - renderItem={({ item, index }) => { - return - { - const route = routes!.find((route) => route.shortName === item.routeNumber); - - if (route) { - dismissSheet("stopTimetable") - - setSelectedRoute(route); - setDrawnRoutes([route]); - presentSheet("routeDetails"); - } - }} - tintColor={getLineColor(item.routeNumber)} - stopCode={selectedStop?.stopCode ?? ""} - /> - - }} - /> - - )} - - {nonRouteSchedules && nonRouteSchedules.length > 0 && ( - // show other routes button - setShowNonRouteSchedules(!showNonRouteSchedules)} - > - {showNonRouteSchedules ? "Hide" : "Show"} Other Routes - - )} - - )} - - ) -} - -export default StopTimetable; \ No newline at end of file +export default StopTimetable; diff --git a/app/components/sheets/route_planning/InputRoute.tsx b/app/components/sheets/route_planning/InputRoute.tsx index 2d256ab8..17684c4a 100644 --- a/app/components/sheets/route_planning/InputRoute.tsx +++ b/app/components/sheets/route_planning/InputRoute.tsx @@ -1,300 +1,484 @@ -import React, { memo, useEffect, useState } from "react"; -import { View, Text, TouchableOpacity, Keyboard, ActivityIndicator, Button, Platform } from "react-native"; -import { BottomSheetModal, BottomSheetView, BottomSheetFlatList } from "@gorhom/bottom-sheet"; +import React, { memo, useEffect, useState } from 'react'; +import { + View, + Text, + TouchableOpacity, + Keyboard, + ActivityIndicator, + Button, + Platform, + Linking, +} from 'react-native'; +import { BottomSheetModal, BottomSheetFlatList } from '@gorhom/bottom-sheet'; import Ionicons from '@expo/vector-icons/Ionicons'; -import useAppStore from "../../../data/app_state"; -import SheetHeader from "../../ui/SheetHeader"; -import { MyLocationSuggestion, SearchSuggestion } from "utils/interfaces"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import SuggestionInput from "app/components/ui/SuggestionInput"; -import SegmentedControl from "@react-native-segmented-control/segmented-control"; -import TimeInput from "app/components/ui/TimeInput"; -import { useTripPlan } from "app/data/api_query"; -import { useQueryClient } from "@tanstack/react-query"; -import TripPlanCell from "app/components/ui/TripPlanCell"; +import useAppStore from '../../../data/app_state'; +import SheetHeader from '../../ui/SheetHeader'; +import { MyLocationSuggestion, SearchSuggestion } from 'utils/interfaces'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import SuggestionInput from 'app/components/ui/SuggestionInput'; +import SegmentedControl from '@react-native-segmented-control/segmented-control'; +import TimeInput from 'app/components/ui/TimeInput'; +import { useTripPlan } from 'app/data/api_query'; +import { useQueryClient } from '@tanstack/react-query'; +import TripPlanCell from 'app/components/ui/TripPlanCell'; import * as Location from 'expo-location'; -import { Linking } from "react-native"; - -interface SheetProps { - sheetRef: React.RefObject -} +import { SheetProps } from 'app/utils'; // AlertList (for all routes and current route) const InputRoute: React.FC = ({ sheetRef }) => { - - const snapPoints = ['85%']; - - const theme = useAppStore((state) => state.theme); - - // Planning Inputs - const [startLocation, setStartLocation] = useState(MyLocationSuggestion); - const [endLocation, setEndLocation] = useState(null); - const [deadline, setDeadline] = useState<"leave" | "arrive">("leave"); - const [time, setTime] = useState(new Date()); - - const { data: tripPlan, isError, isLoading: tripPlanLoading } = useTripPlan(startLocation, endLocation, time, deadline); - - const searchSuggestions = useAppStore((state) => state.suggestions); - const suggestionOutput = useAppStore((state) => state.suggestionOutput); - const setSuggestions = useAppStore((state) => state.setSuggestions); - const setSuggesionOutput = useAppStore((state) => state.setSuggestionOutput); - const [routeInfoError, setRouteInfoError] = useState(""); - const dismissSheet = useAppStore((state) => state.dismissSheet); - const setSheetCloseCallback = useAppStore((state) => state.setSheetCloseCallback); - - const [searchSuggestionsLoading, setSearchSuggestionsLoading] = useState(false) - - const client = useQueryClient() - - // // Favorite Location - // const { data: favoriteLocations, refetch: refetchFavoriteLocations } = useFavoriteLocations(); - // const addLocationFavorite = addFavoriteLocationMutation(); - // const removeLocationFavorite = removeFavoriteLocationMutation(); - - const [timeInputFocused, setTimeInputFocused] = useState(false); - const [segmentedIndex, setSegmentedIndex] = useState(0) - - // function toggleFavoriteLocation(location: SearchSuggestion) { - // if (favoriteLocations && favoriteLocations.find((item) => suggestionEqual(item, location))) - // { - // removeLocationFavorite.mutate(location) - // } else { - // addLocationFavorite.mutate(location) - // } - - // refetchFavoriteLocations() - // } - - function toggleTimeInputFocused(newValue: boolean) { - setTimeInputFocused(newValue) + const snapPoints = ['85%']; + + const theme = useAppStore((state) => state.theme); + + // Planning Inputs + const [startLocation, setStartLocation] = useState( + MyLocationSuggestion, + ); + const [endLocation, setEndLocation] = useState(null); + const [deadline, setDeadline] = useState<'leave' | 'arrive'>('leave'); + const [time, setTime] = useState(new Date()); + + const { + data: tripPlan, + isError, + isLoading: tripPlanLoading, + } = useTripPlan(startLocation, endLocation, time, deadline); + + const searchSuggestions = useAppStore((state) => state.suggestions); + const suggestionOutput = useAppStore((state) => state.suggestionOutput); + const setSuggestions = useAppStore((state) => state.setSuggestions); + const setSuggesionOutput = useAppStore((state) => state.setSuggestionOutput); + const [routeInfoError, setRouteInfoError] = useState(''); + const dismissSheet = useAppStore((state) => state.dismissSheet); + const setSheetCloseCallback = useAppStore( + (state) => state.setSheetCloseCallback, + ); + + const [searchSuggestionsLoading, setSearchSuggestionsLoading] = + useState(false); + + const client = useQueryClient(); + + // // Favorite Location + // const { data: favoriteLocations, refetch: refetchFavoriteLocations } = useFavoriteLocations(); + // const addLocationFavorite = addFavoriteLocationMutation(); + // const removeLocationFavorite = removeFavoriteLocationMutation(); + + const [timeInputFocused, setTimeInputFocused] = useState(false); + const [segmentedIndex, setSegmentedIndex] = useState(0); + + // function toggleFavoriteLocation(location: SearchSuggestion) { + // if (favoriteLocations && favoriteLocations.find((item) => suggestionEqual(item, location))) + // { + // removeLocationFavorite.mutate(location) + // } else { + // addLocationFavorite.mutate(location) + // } + + // refetchFavoriteLocations() + // } + + function toggleTimeInputFocused(newValue: boolean) { + setTimeInputFocused(newValue); + } + + useEffect(() => { + setSheetCloseCallback(() => { + setTimeout(() => { + setStartLocation(MyLocationSuggestion); + setEndLocation(null); + setSuggesionOutput(null); + }, 500); + }, 'inputRoute'); + }, []); + + useEffect(() => { + if (suggestionOutput) { + setRouteInfoError(''); + return; } - useEffect(() => { - setSheetCloseCallback(() => { - setTimeout(() => { - setStartLocation(MyLocationSuggestion) - setEndLocation(null) - setSuggesionOutput(null) - }, 500) - }, "inputRoute") - }, []) - - useEffect(() => { - - if (suggestionOutput) { - setRouteInfoError(""); - return - } + if (startLocation && endLocation) { + if (startLocation.title === endLocation.title) { + setRouteInfoError('Start and End locations cannot be the same'); + return; + } + } - if (startLocation && endLocation) { - if (startLocation.title == endLocation.title) { - setRouteInfoError("Start and End locations cannot be the same"); - return - } + if ( + startLocation?.type === 'my-location' || + endLocation?.type === 'my-location' + ) { + // Request location permissions + Location.requestForegroundPermissionsAsync().then(async (status) => { + // Check if permission is granted + if (!status.granted) { + setRouteInfoError( + 'Location Unavailable, enable location in Settings.', + ); + return; } - if (startLocation?.type == "my-location" || endLocation?.type == "my-location") { - // Request location permissions - Location.requestForegroundPermissionsAsync().then(async (status) => { - // Check if permission is granted - if (!status.granted) { - setRouteInfoError("Location Unavailable, enable location in Settings.") - return - } - - // Get current location - const locationCoords = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced, timeInterval: 2 }); + // Get current location + const locationCoords = await Location.getCurrentPositionAsync({ + accuracy: Location.Accuracy.Balanced, + timeInterval: 2, + }); - // Set the location coordinates - let location = MyLocationSuggestion - location.lat = locationCoords.coords.latitude - location.long = locationCoords.coords.longitude + // Set the location coordinates + let location = MyLocationSuggestion; + location.lat = locationCoords.coords.latitude; + location.long = locationCoords.coords.longitude; - if (startLocation?.type == "my-location") setStartLocation(location) - if (endLocation?.type == "my-location") setEndLocation(location) - }) - } + if (startLocation?.type === 'my-location') setStartLocation(location); + if (endLocation?.type === 'my-location') setEndLocation(location); + }); + } - setRouteInfoError(""); - }, [startLocation, endLocation, suggestionOutput]) - - return ( - - { - if (!suggestionOutput && !timeInputFocused) Keyboard.dismiss() + setRouteInfoError(''); + }, [startLocation, endLocation, suggestionOutput]); + + return ( + + { + if (!suggestionOutput && !timeInputFocused) Keyboard.dismiss(); + }} + style={[!(routeInfoError === '') && { flex: 1 }]} + > + {/* header */} + { + Keyboard.dismiss(); + dismissSheet('inputRoute'); + }} + > + + + } + /> + + {/* Route Details Input */} + + {/* Endpoint Input */} + + + {/* Start */} + { + if (startLocation?.type === 'my-location') + setStartLocation(null); + }} + icon={ + startLocation?.type === 'my-location' ? ( + + ) : ( + + ) + } + setSuggestionLoading={setSearchSuggestionsLoading} + /> + + {/* 2 dots in between rows */} + + + + + {/* End */} + { + if (endLocation?.type === 'my-location') setEndLocation(null); + }} + icon={ + endLocation?.type === 'my-location' ? ( + + ) : ( + + ) + } + setSuggestionLoading={setSearchSuggestionsLoading} + /> + + + {/* Swap Endpoints */} + { + const temp = startLocation; + setStartLocation(endLocation); + setEndLocation(temp); + setSuggesionOutput(null); + }} + > + + + + + {/* Leave by/Arrive By */} + + { + setSegmentedIndex(event.nativeEvent.selectedSegmentIndex); + setDeadline( + event.nativeEvent.selectedSegmentIndex === 0 + ? 'leave' + : 'arrive', + ); + }} + style={{ flex: 1, marginRight: 8 }} + backgroundColor={ + Platform.OS === 'android' + ? theme.androidSegmentedBackground + : undefined + } + /> + + setTime(time)} + onTimeInputFocused={toggleTimeInputFocused} + /> + + + {/* Divider */} + + + {/* Error */} + {routeInfoError !== '' && ( + - {/* header */} - { - Keyboard.dismiss() - dismissSheet("inputRoute") - }} - > - - - } + {/* Error Text */} + + {routeInfoError} + + + {routeInfoError === + 'Location Unavailable, enable location in Settings.' && ( +