diff --git a/app/components/calendar-icon/calendar-icon.tsx b/app/components/calendar-icon/calendar-icon.tsx new file mode 100644 index 00000000..3753a734 --- /dev/null +++ b/app/components/calendar-icon/calendar-icon.tsx @@ -0,0 +1,25 @@ +import { Image, ImageStyle, TouchableOpacity, ViewStyle } from "react-native" +import { translate } from "../../i18n" + +const CalenderImageIcon = require("../../../assets/calendar.png") + +const ICON_STYLE: ImageStyle = { + width: 27, + height: 25, + resizeMode: "contain", + tintColor: "lightgrey", + opacity: 0.9, +} +const CONTAINER: ViewStyle = { + justifyContent: "center", +} + +export const CalendarIcon = function CalendarIcon(props: { style?: ViewStyle; onPress: () => void }) { + const { onPress, style } = props + + return ( + + + + ) +} diff --git a/app/components/calendar-icon/index.ts b/app/components/calendar-icon/index.ts new file mode 100644 index 00000000..a2b5cc95 --- /dev/null +++ b/app/components/calendar-icon/index.ts @@ -0,0 +1 @@ +export * from "./calendar-icon" diff --git a/app/components/icon/icon.props.ts b/app/components/icon/icon.props.ts deleted file mode 100644 index 6928ede2..00000000 --- a/app/components/icon/icon.props.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ImageStyle, ViewStyle } from "react-native" -import { IconTypes } from "./icons" - -export interface IconProps { - /** - * Style overrides for the icon image - */ - style?: ImageStyle - - /** - * Style overrides for the icon container - */ - - containerStyle?: ViewStyle - - /** - * The name of the icon - */ - - icon?: IconTypes -} diff --git a/app/components/icon/icon.story.tsx b/app/components/icon/icon.story.tsx deleted file mode 100644 index d119ed40..00000000 --- a/app/components/icon/icon.story.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from "react" -import { storiesOf } from "@storybook/react-native" -import { StoryScreen, Story, UseCase } from "../../../storybook/views" -import { Icon } from "./icon" - -declare let module - -storiesOf("Icon", module) - .addDecorator((fn) => {fn()}) - .add("Names", () => ( - - - - - - - - - )) diff --git a/app/components/icon/icon.tsx b/app/components/icon/icon.tsx deleted file mode 100644 index c2d9723f..00000000 --- a/app/components/icon/icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from "react" -import { View, Image, ImageStyle } from "react-native" -import { IconProps } from "./icon.props" -import { icons } from "./icons" - -const ROOT: ImageStyle = { - resizeMode: "contain", -} - -export function Icon(props: IconProps) { - const { style: styleOverride, icon, containerStyle } = props - const style: ImageStyle = { ...ROOT, ...styleOverride } - - return ( - - - - ) -} diff --git a/app/components/icon/icons/arrow-left.png b/app/components/icon/icons/arrow-left.png deleted file mode 100644 index 9d607d76..00000000 Binary files a/app/components/icon/icons/arrow-left.png and /dev/null differ diff --git a/app/components/icon/icons/arrow-left@2x.png b/app/components/icon/icons/arrow-left@2x.png deleted file mode 100644 index 9d607d76..00000000 Binary files a/app/components/icon/icons/arrow-left@2x.png and /dev/null differ diff --git a/app/components/icon/icons/bullet.png b/app/components/icon/icons/bullet.png deleted file mode 100644 index 8fc256f6..00000000 Binary files a/app/components/icon/icons/bullet.png and /dev/null differ diff --git a/app/components/icon/icons/bullet@2x.png b/app/components/icon/icons/bullet@2x.png deleted file mode 100644 index 8fc256f6..00000000 Binary files a/app/components/icon/icons/bullet@2x.png and /dev/null differ diff --git a/app/components/icon/icons/index.ts b/app/components/icon/icons/index.ts deleted file mode 100644 index 00e8a591..00000000 --- a/app/components/icon/icons/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const icons = { - back: require("./arrow-left.png"), - bullet: require("./bullet.png"), -} - -export type IconTypes = keyof typeof icons diff --git a/app/components/index.ts b/app/components/index.ts index 84d5048e..4ec6667a 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -1,6 +1,5 @@ export * from "./button/button" export * from "./close-button/close-button" -export * from "./icon/icon" export * from "./screen/screen" export * from "./text/text" export * from "./dummy-input/dummy-input" diff --git a/app/components/route-details-header/route-details-header.tsx b/app/components/route-details-header/route-details-header.tsx index 7097a9cf..9e0dcd78 100644 --- a/app/components/route-details-header/route-details-header.tsx +++ b/app/components/route-details-header/route-details-header.tsx @@ -12,6 +12,9 @@ import { stationsObject, stationLocale } from "../../data/stations" import { isRTL, translate } from "../../i18n" import { useStores } from "../../models" import * as Burnt from "burnt" +import * as AddCalendarEvent from "react-native-add-calendar-event" +import { CalendarIcon } from "../calendar-icon/calendar-icon" +import { RouteItem } from "../../services/api" const arrowIcon = require("../../../assets/arrow-left.png") @@ -84,48 +87,60 @@ const HEADER_RIGHT_WRAPPER: ViewStyle = { export interface RouteDetailsHeaderProps { originId: string destinationId: string + routeItem: RouteItem /** * The screen name we're displaying the header inside */ screenName?: "routeDetails" | "activeRide" style?: ViewStyle + eventConfig?: AddCalendarEvent.CreateOptions } export const RouteDetailsHeader = observer(function RouteDetailsHeader(props: RouteDetailsHeaderProps) { - const { originId, destinationId, screenName, style } = props + const { routeItem, originId, destinationId, screenName, style, eventConfig } = props const { favoriteRoutes } = useStores() const navigation = useNavigation() + const addToCalendar = () => { + const eventConfig = createEventConfig(routeItem) + AddCalendarEvent.presentEventCreatingDialog(eventConfig) + } + const originName = stationsObject[originId][stationLocale] const destinationName = stationsObject[destinationId][stationLocale] const routeId = `${originId}${destinationId}` - const isFavorite: boolean = useMemo(() => { - return favoriteRoutes.routes.find((favorite) => favorite.id === routeId) - }, [favoriteRoutes.routes.length]) + const isFavorite: boolean = useMemo( + () => !!favoriteRoutes.routes.find((favorite) => favorite.id === routeId), + [favoriteRoutes.routes.length], + ) useLayoutEffect(() => { screenName !== "activeRide" && navigation.setOptions({ headerRight: () => ( - { - const favorite = { id: routeId, originId, destinationId } - if (!isFavorite) { - Burnt.alert({ title: translate("favorites.added"), duration: 1.5 }) - HapticFeedback.trigger("impactMedium") - favoriteRoutes.add(favorite) - analytics().logEvent("favorite_route_added") - } else { - HapticFeedback.trigger("impactLight") - favoriteRoutes.remove(favorite.id) - analytics().logEvent("favorite_route_removed") - } - }} - /> + {screenName === "routeDetails" ? ( + + ) : ( + { + const favorite = { id: routeId, originId, destinationId } + if (!isFavorite) { + Burnt.alert({ title: translate("favorites.added"), duration: 1.5 }) + HapticFeedback.trigger("impactMedium") + favoriteRoutes.add(favorite) + analytics().logEvent("favorite_route_added") + } else { + HapticFeedback.trigger("impactLight") + favoriteRoutes.remove(favorite.id) + analytics().logEvent("favorite_route_removed") + } + }} + /> + )} ), }) @@ -160,3 +175,20 @@ export const RouteDetailsHeader = observer(function RouteDetailsHeader(props: Ro ) }) + +function createEventConfig(routeItem: RouteItem) { + const { destinationStationName: destination, originStationName: origin, trainNumber } = routeItem.trains[0] + + const title = translate("plan.eventTitle", { destination }) + const notes = translate("plan.trainFromToStation", { trainNumber, origin, destination }) + + const eventConfig: AddCalendarEvent.CreateOptions = { + title, + startDate: new Date(routeItem.departureTime).toISOString(), + endDate: new Date(routeItem.arrivalTime).toISOString(), + location: translate("plan.trainStation", { stationName: origin }), + notes, + } + + return eventConfig +} diff --git a/app/components/star-icon/star-icon.tsx b/app/components/star-icon/star-icon.tsx index f55b3966..ba871216 100644 --- a/app/components/star-icon/star-icon.tsx +++ b/app/components/star-icon/star-icon.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from "react" import { Image, TouchableOpacity, ImageStyle, ViewStyle } from "react-native" import { observer } from "mobx-react-lite" -import { color, spacing } from "../../theme" +import { color } from "../../theme" const starImage = require("../../../assets/star.png") @@ -46,3 +46,4 @@ export const StarIcon = observer(function StarIcon(props: StarIconProps) { ) }) + diff --git a/app/i18n/ar.json b/app/i18n/ar.json index 36c84135..d640636c 100644 --- a/app/i18n/ar.json +++ b/app/i18n/ar.json @@ -20,7 +20,10 @@ "find": "حساب المسار", "resetTime": "إعادة تعيين الوقت", "switchStations": "تبديل المحطات", - "switchStationsHint": "اختصار لتبديل محطة البداية بمحطة الوصول" + "switchStationsHint": "اختصار لتبديل محطة البداية بمحطة الوصول", + "trainFromToStation": "القطار رقم %{trainNumber} من %{origin} إلى %{destination}", + "eventTitle": "رحلة إلى %{destination}", + "trainStation": "محطة القطار %{stationName}" }, "selectStation": { "placeholder": "البحث عن محطة", @@ -47,7 +50,8 @@ "waitingTime": "مدة الانتظار", "lastStop": "المحطة الأخيرة", "routeWarning": "تحذير الطريق الطويل", - "routeWarningText": "هناك طرق قطار أقصر حول وقت المغادرة." + "routeWarningText": "هناك طرق قطار أقصر حول وقت المغادرة.", + "addToCalendar": "إضافة إلى التقويم" }, "settings": { "title": "اعدادت", diff --git a/app/i18n/en.json b/app/i18n/en.json index 4a1fbfb2..898479af 100644 --- a/app/i18n/en.json +++ b/app/i18n/en.json @@ -24,7 +24,10 @@ "find": "Find Routes", "resetTime": "Reset Time", "switchStations": "Flip stations", - "switchStationsHint": "A shortcut to flip the origin station with the destination station" + "switchStationsHint": "A shortcut to flip the origin station with the destination station", + "trainFromToStation": "Train No. %{trainNumber} from %{origin} to %{destination}", + "eventTitle": "Ride to %{destination}", + "trainStation": "%{stationName} Train Station" }, "selectStation": { "placeholder": "Search Stations", @@ -55,7 +58,8 @@ "minutes": "min", "lastStop": "Last Stop", "routeWarning": "Long Route Warning", - "routeWarningText": "There are shorter train routes around the departure time." + "routeWarningText": "There are shorter train routes around the departure time.", + "addToCalendar": "Add to Calendar" }, "settings": { "title": "Settings", diff --git a/app/i18n/he.json b/app/i18n/he.json index 728b3a59..b6473e44 100644 --- a/app/i18n/he.json +++ b/app/i18n/he.json @@ -24,7 +24,10 @@ "find": "חישוב מסלול", "resetTime": "איפוס זמן", "switchStations": "החלפת תחנות", - "switchStationsHint": "קיצור דרך להחלפת תחנת המוצא עם תחנת היעד" + "switchStationsHint": "קיצור דרך להחלפת תחנת המוצא עם תחנת היעד", + "trainFromToStation": "רכבת מס' %{trainNumber} מ%{origin} אל %{destination}", + "eventTitle": "נסיעה ל%{destination}", + "trainStation": "תחנת רכבת %{stationName}" }, "selectStation": { "placeholder": "חיפוש תחנה", @@ -55,7 +58,8 @@ "minutes": "דק'", "lastStop": "יעד סופי", "routeWarning": "רכבת מאספת", - "routeWarningText": "קיימים מסלולי נסיעה קצרים יותר סביב שעת היציאה הנבחרת" + "routeWarningText": "קיימים מסלולי נסיעה קצרים יותר סביב שעת היציאה הנבחרת", + "addToCalendar": "הוספה ליומן" }, "settings": { "title": "הגדרות", diff --git a/app/i18n/ru.json b/app/i18n/ru.json index b19b83f3..13bb3a29 100644 --- a/app/i18n/ru.json +++ b/app/i18n/ru.json @@ -24,7 +24,10 @@ "find": "Найти маршруты", "resetTime": "сбросить время", "switchStations": "Перевернуть станции", - "switchStationsHint": "Ярлык для перестановки исходной станции со станцией назначения" + "switchStationsHint": "Ярлык для перестановки исходной станции со станцией назначения", + "trainFromToStation": "Поезд № %{trainNumber} из %{origin} в %{destination}", + "eventTitle": "Поездка в %{destination}", + "trainStation": "%{stationName} станция" }, "selectStation": { "placeholder": "Поиск остановки", @@ -54,7 +57,8 @@ "minutes": "минут", "lastStop": "Последняя остановка", "routeWarning": "Предупреждение о длинном маршруте", - "routeWarningText": "Во время отправления есть более короткие маршруты поездов." + "routeWarningText": "Во время отправления есть более короткие маршруты поездов.", + "addToCalendar": "Добавить в календарь" }, "settings": { "title": "Настройки", diff --git a/app/screens/route-details/route-details-screen.tsx b/app/screens/route-details/route-details-screen.tsx index f28cbf80..f7dbdb4a 100644 --- a/app/screens/route-details/route-details-screen.tsx +++ b/app/screens/route-details/route-details-screen.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-native/no-inline-styles */ import React, { useEffect, useMemo, useRef, useState } from "react" -import { Platform, View, ViewStyle } from "react-native" +import { View, ViewStyle } from "react-native" import { observer } from "mobx-react-lite" import { useSafeAreaInsets } from "react-native-safe-area-context" import { SharedElement } from "react-navigation-shared-element" @@ -14,17 +14,25 @@ import { useRideProgress } from "../../hooks/use-ride-progress" import { RouteDetailsScreenProps } from "../../navigators/main-navigator" import { color, spacing } from "../../theme" import { RouteDetailsHeader, Screen } from "../../components" -import { RouteStationCard, RouteStopCard, RouteLine, RouteExchangeDetails } from "./components" -import { LiveRideSheet, LongRouteWarning, StartRideButton } from "./components" +import { + LiveRideSheet, + LongRouteWarning, + RouteExchangeDetails, + RouteLine, + RouteStationCard, + RouteStopCard, + StartRideButton, +} from "./components" import BottomSheet from "@gorhom/bottom-sheet" import { FirstRideAlert } from "./components/first-ride-alert" import { canRunLiveActivities } from "../../utils/ios-helpers" +import { CreateOptions } from "react-native-add-calendar-event" +import { translate } from "../../i18n" const ROOT: ViewStyle = { flex: 1, backgroundColor: color.background, } - export const RouteDetailsScreen = observer(function RouteDetailsScreen({ route }: RouteDetailsScreenProps) { const { ride } = useStores() const bottomSheetRef = useRef(null) @@ -66,6 +74,7 @@ export const RouteDetailsScreen = observer(function RouteDetailsScreen({ route } > UIViewControllerBasedStatusBarAppearance + NSCalendarsUsageDescription + We need access to your calendar to schedule events and reminders. + + NSContactsUsageDescription + We need access to your contacts to import and manage contacts within our app. + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ba02401b..80b56247 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -386,6 +386,8 @@ PODS: - React-jsinspector (0.71.7) - React-logger (0.71.7): - glog + - react-native-add-calendar-event (4.2.2): + - React-Core - react-native-blur (4.3.2): - React-Core - react-native-date-picker (4.2.10): @@ -607,6 +609,7 @@ DEPENDENCIES: - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - react-native-add-calendar-event (from `../node_modules/react-native-add-calendar-event`) - "react-native-blur (from `../node_modules/@react-native-community/blur`)" - react-native-date-picker (from `../node_modules/react-native-date-picker`) - react-native-in-app-review (from `../node_modules/react-native-in-app-review`) @@ -724,6 +727,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/jsinspector" React-logger: :path: "../node_modules/react-native/ReactCommon/logger" + react-native-add-calendar-event: + :path: "../node_modules/react-native-add-calendar-event" react-native-blur: :path: "../node_modules/@react-native-community/blur" react-native-date-picker: @@ -853,6 +858,7 @@ SPEC CHECKSUMS: React-jsiexecutor: eaa5f71eb8f6861cf0e57f1a0f52aeb020d9e18e React-jsinspector: 9885f6f94d231b95a739ef7bb50536fb87ce7539 React-logger: 3f8ebad1be1bf3299d1ab6d7f971802d7395c7ef + react-native-add-calendar-event: 34ad00f629187cbb97c2f04195e17803b2b3bc5e react-native-blur: cfdad7b3c01d725ab62a8a729f42ea463998afa2 react-native-date-picker: 89c419d88fa8e3c5f2d3cbae0bb7f7454ae8ea55 react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4 diff --git a/package.json b/package.json index 32a86444..3f9e5cd2 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "ramda": "0.27.1", "react": "18.2.0", "react-native": "0.71.7", + "react-native-add-calendar-event": "^4.2.2", "react-native-bouncy-checkbox": "^3.0.7", "react-native-date-picker": "4.2.10", "react-native-device-info": "10.6.0", diff --git a/storybook/storybook-registry.ts b/storybook/storybook-registry.ts index 1e59d883..789da6c5 100644 --- a/storybook/storybook-registry.ts +++ b/storybook/storybook-registry.ts @@ -1,6 +1,5 @@ require("../app/components/text/text.story") require("../app/components/button/button.story") -require("../app/components/icon/icon.story") require("../app/components/dummy-input/dummy-input.story") require("../app/components/button/button.story") require("../app/components/station-card/station-card.story") diff --git a/yarn.lock b/yarn.lock index 3e279d8f..7a9a3d7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16219,6 +16219,11 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-native-add-calendar-event@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/react-native-add-calendar-event/-/react-native-add-calendar-event-4.2.2.tgz#c9b7a658358d39d9e577e22fb43ed74b0b34c201" + integrity sha512-y8Us+DDvSZL3ndWJhwKisLG3gml9Ju4FoHQ2rNQyysa9vPRZwfNXsk1tRlSjJ7ZIrxWUwOrVbXe98PN44N+qsQ== + react-native-animatable@1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.3.3.tgz"