├── .env.example ├── .expo-shared ├── README.md └── assets.json ├── .gitignore ├── @types ├── env.d.ts └── navigation.d.ts ├── App.tsx ├── README.md ├── _prints ├── demo.gif ├── print01.jpeg ├── print02.jpeg ├── print03.jpeg ├── print04.jpeg ├── print05.jpeg └── print06.jpeg ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── icon.png ├── images │ ├── marker.png │ ├── marker@2x.png │ ├── marker@3x.png │ ├── markercircle.png │ ├── markercircle@2x.png │ └── markercircle@3x.png ├── login_background.png └── splash.png ├── babel.config.js ├── package-lock.json ├── package.json ├── src ├── apis │ └── index.ts ├── components │ ├── Chip │ │ └── index.tsx │ ├── Divider │ │ └── index.tsx │ ├── LocationCard │ │ └── index.tsx │ ├── Map │ │ └── index.js │ ├── MapDirections │ │ └── index.tsx │ ├── NavFavourites │ │ └── index.tsx │ ├── NavOptions │ │ └── index.tsx │ ├── PlaceAutoComplete │ │ └── index.tsx │ ├── Separator │ │ └── index.tsx │ └── UberLogo │ │ └── index.tsx ├── models │ ├── Coordinates.ts │ ├── Direction.ts │ ├── MatrixDistance.ts │ ├── Place.ts │ ├── TravelTime.ts │ └── User.ts ├── routes │ ├── index.tsx │ └── options.ts ├── screens │ ├── EatsScreen.tsx │ ├── HomeScreen.tsx │ ├── LoginScreen.tsx │ ├── MapScreen.tsx │ ├── NavigateCardScreen.tsx │ └── RideOptionsCardScreen.tsx ├── services │ ├── DirectionsService.ts │ ├── GeocodingService.ts │ └── MatrixService.ts ├── stores │ ├── DirectionsStore.ts │ └── UserStore.ts └── utils │ ├── ellipsisText.ts │ ├── pixelsSize.ts │ └── timeCalculator.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | MAPBOX_APIKEY=pk.YOUR_MAPBOX_PUBLIC_KEY -------------------------------------------------------------------------------- /.expo-shared/README.md: -------------------------------------------------------------------------------- 1 | > Why do I have a folder named ".expo-shared" in my project? 2 | 3 | The ".expo-shared" folder is created when running commands that produce state that is intended to be shared with all developers on the project. For example, "npx expo-optimize". 4 | 5 | > What does the "assets.json" file contain? 6 | 7 | The "assets.json" file describes the assets that have been optimized through "expo-optimize" and do not need to be processed again. 8 | 9 | > Should I commit the ".expo-shared" folder? 10 | 11 | Yes, you should share the ".expo-shared" folder with your collaborators. 12 | -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | .env 12 | web-build/ 13 | 14 | # macOS 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /@types/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-native-dotenv' { 2 | export const MAPBOX_APIKEY: string; 3 | export const ENV: 'dev' | 'prod'; 4 | } -------------------------------------------------------------------------------- /@types/navigation.d.ts: -------------------------------------------------------------------------------- 1 | import { RootStackParamList } from '../src/routes'; 2 | 3 | declare global { 4 | namespace ReactNavigation { 5 | interface RootParamList extends RootStackParamList {} 6 | } 7 | } -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Text, View } from 'react-native'; 2 | import { SafeAreaProvider } from 'react-native-safe-area-context'; 3 | import { KeyboardAvoidingView, Platform } from 'react-native'; 4 | import { Routes } from './src/routes'; 5 | 6 | export default function App() { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uber Clone TypeScript 2 | Clone da aplicação Uber utilizando as tecnologias: 3 | 4 | React Native | Tailwind | Zustand | Mapbox API | Maps | Navigation | Directions 5 | 6 | ## Destaques Implementados: 7 | - Zero custo para utilizar as API, direfente do Google Maps onde é necessário utilizar o cartão de crédito 0️⃣. 8 | - Tailwind com React Native 🍀. 9 | - Zustand para gerenciamento de estado das cooredenas de origem e destino 🐻. 10 | - Navegação entre rotas com React Navigation 🛣️. 11 | - Matrix API para calcular tempo de viagem e preço. 12 | - Directions API para traçar uma rota de ponto origem e destino. 13 | - Os locais de busca são de acordo com a localização atual do GPS do usuário 📍. 14 | - Técnica de debounce para buscar as rotas conforme o usuário digita. 15 | - Poline para desenhar a linha das rotas de origem e destino. 16 | 17 | ## Instalação 18 | 1) Instale o Expo ```npm install --global expo-cli``` 19 | 2) Registre sua conta no ```Mapbox (é gratuito!)``` e copie a ```public key```; 20 | 2) Copie o .env.example para .env e cole sua chave pública do Mapbox; 21 | 3) Rode o comando ```npm install``` para instalar as dependências; 22 | 4) Execute o Expo com comando ```npm run start``` (certifique-se de ter o aplicativo do Expo instalado e os SDKs Android ou IOS); 23 | 24 | 25 | ## Images 26 |
27 | Login 28 | Navegação 29 | Rotas 30 | Uber X 31 | Mapa destino 32 | Selecionando a corrida 33 |
34 | 35 |
36 | 37 |
38 | Demo 39 |
40 | -------------------------------------------------------------------------------- /_prints/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/_prints/demo.gif -------------------------------------------------------------------------------- /_prints/print01.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/_prints/print01.jpeg -------------------------------------------------------------------------------- /_prints/print02.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/_prints/print02.jpeg -------------------------------------------------------------------------------- /_prints/print03.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/_prints/print03.jpeg -------------------------------------------------------------------------------- /_prints/print04.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/_prints/print04.jpeg -------------------------------------------------------------------------------- /_prints/print05.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/_prints/print05.jpeg -------------------------------------------------------------------------------- /_prints/print06.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/_prints/print06.jpeg -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "vuber-ts", 4 | "slug": "vuber-ts", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "splash": { 9 | "image": "./assets/splash.png", 10 | "resizeMode": "contain", 11 | "backgroundColor": "#ffffff" 12 | }, 13 | "updates": { 14 | "fallbackToCacheTimeout": 0 15 | }, 16 | "assetBundlePatterns": [ 17 | "**/*" 18 | ], 19 | "ios": { 20 | "supportsTablet": true 21 | }, 22 | "android": { 23 | "adaptiveIcon": { 24 | "foregroundImage": "./assets/adaptive-icon.png", 25 | "backgroundColor": "#FFFFFF" 26 | } 27 | }, 28 | "web": { 29 | "favicon": "./assets/favicon.png" 30 | }, 31 | "description": "https://github.com/pvictorf/uber_reactnative_ts", 32 | "githubUrl": "https://github.com/pvictorf/uber_reactnative_ts" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/assets/icon.png -------------------------------------------------------------------------------- /assets/images/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/assets/images/marker.png -------------------------------------------------------------------------------- /assets/images/marker@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/assets/images/marker@2x.png -------------------------------------------------------------------------------- /assets/images/marker@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/assets/images/marker@3x.png -------------------------------------------------------------------------------- /assets/images/markercircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/assets/images/markercircle.png -------------------------------------------------------------------------------- /assets/images/markercircle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/assets/images/markercircle@2x.png -------------------------------------------------------------------------------- /assets/images/markercircle@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/assets/images/markercircle@3x.png -------------------------------------------------------------------------------- /assets/login_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/assets/login_background.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvictorf/uber_reactnative_ts/34f2bc8c039190492ab3f3faeaf5ad1ab39b96e3/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: [ 6 | ["module:react-native-dotenv", { 7 | "moduleName": "react-native-dotenv" 8 | }], 9 | ], 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuber-ts", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web", 10 | "eject": "expo eject" 11 | }, 12 | "dependencies": { 13 | "@react-native-community/masked-view": "^0.1.11", 14 | "@react-navigation/native": "^6.0.10", 15 | "@react-navigation/native-stack": "^6.6.1", 16 | "@react-navigation/stack": "^6.2.1", 17 | "@types/lodash": "^4.14.181", 18 | "@types/react-native-dotenv": "^0.2.0", 19 | "axios": "^0.26.1", 20 | "expo": "~44.0.0", 21 | "expo-constants": "~13.0.1", 22 | "expo-location": "~14.0.1", 23 | "expo-status-bar": "~1.2.0", 24 | "intl": "^1.2.5", 25 | "lodash": "^4.17.21", 26 | "react": "17.0.1", 27 | "react-native": "0.64.3", 28 | "react-native-gesture-handler": "^2.1.3", 29 | "react-native-maps": "^0.29.4", 30 | "react-native-reanimated": "^2.6.0", 31 | "react-native-safe-area-context": "^3.3.2", 32 | "react-native-screens": "^3.10.2", 33 | "twrnc": "3.0.2", 34 | "zustand": "^3.7.2" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.12.9", 38 | "@types/react": "~17.0.21", 39 | "@types/react-native": "~0.64.12", 40 | "react-native-dotenv": "^3.3.1", 41 | "typescript": "~4.3.5" 42 | }, 43 | "private": true 44 | } 45 | -------------------------------------------------------------------------------- /src/apis/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MAPBOX_APIKEY } from 'react-native-dotenv'; 3 | 4 | export const geocodingApi = axios.create({ 5 | baseURL: `https://api.mapbox.com/geocoding/v5`, 6 | params: { 7 | 'access_token': MAPBOX_APIKEY 8 | } 9 | }) 10 | 11 | export const directionsApi = axios.create({ 12 | baseURL: `https://api.mapbox.com/directions/v5/mapbox`, 13 | params: { 14 | 'access_token': MAPBOX_APIKEY 15 | } 16 | }) 17 | 18 | export const matrixApi = axios.create({ 19 | baseURL: `https://api.mapbox.com/directions-matrix/v1`, 20 | params: { 21 | 'access_token': MAPBOX_APIKEY 22 | } 23 | }) -------------------------------------------------------------------------------- /src/components/Chip/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TouchableOpacity, Text, ViewStyle, StyleProp } from 'react-native'; 3 | import tw from 'twrnc'; 4 | 5 | interface ChipProps { 6 | onPress: () => void, 7 | bgColor: string, 8 | textColor: string, 9 | text: string, 10 | style?: any, 11 | disabled?: boolean, 12 | icon?: JSX.Element, 13 | } 14 | 15 | export const Chip = ({onPress, style, bgColor, textColor, text, icon, disabled}: ChipProps) => { 16 | return ( 17 | 26 | {icon} 27 | {text} 28 | 29 | ); 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/components/Divider/index.tsx: -------------------------------------------------------------------------------- 1 | import {View} from 'react-native'; 2 | 3 | interface DividerProps { 4 | bgColor?: string, 5 | children?: JSX.Element, 6 | height?: number, 7 | } 8 | 9 | export const Divider = ({bgColor = '#ddd', height = 0.6, children}: DividerProps) => { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/components/LocationCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | import { ellipsisText } from '../../utils/ellipsisText'; 3 | import { TravelTime } from '../../models/TravelTime'; 4 | import tw from 'twrnc'; 5 | import IconIonic from '@expo/vector-icons/Ionicons'; 6 | 7 | 8 | export interface LocationCardProps { 9 | placeName: string, 10 | travel: TravelTime, 11 | } 12 | 13 | export const LocationCard = ({placeName, travel}: LocationCardProps) => { 14 | if(!travel.totalSeconds) return null; 15 | 16 | return ( 17 | 18 | 19 | {Number(travel.hours) >= 1 ? `${travel.hours}` : `${travel.minutes}`} 20 | {Number(travel.hours) >= 1 ? 'HRS' : 'MIN'} 21 | 22 | 23 | 24 | {ellipsisText(placeName, 30)} 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Map/index.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import MapView, { Marker } from 'react-native-maps'; 3 | import { useDirectionsStore } from '../../stores/DirectionsStore'; 4 | import { useNavigation } from '@react-navigation/native'; 5 | import tw from 'twrnc' 6 | 7 | import { MapDirections } from '../MapDirections'; 8 | import { MatrixService } from '../../services/MatrixService'; 9 | import { LocationCard } from '../LocationCard'; 10 | import markerImage from '../../../assets/images/marker.png'; 11 | import markerCircleImage from '../../../assets/images/markercircle.png'; 12 | 13 | 14 | export const Map = () => { 15 | const mapRef = useRef(); 16 | const navigation = useNavigation() 17 | const origin = useDirectionsStore(state => state.origin); 18 | const destination = useDirectionsStore(state => state.destination); 19 | const setTravelTimeInformation = useDirectionsStore(state => state.setTravelTimeInformation); 20 | const travelTimeInformation = useDirectionsStore(state => state.travelTimeInformation); 21 | 22 | 23 | useEffect(() => { 24 | async function getTimeTravel() { 25 | if(!origin?.placeName || !destination?.placeName) return; 26 | 27 | const matrix = await MatrixService.findMatrixDuration(origin, destination); 28 | 29 | setTravelTimeInformation({ 30 | ...matrix.travelTimeInformation 31 | }); 32 | 33 | } 34 | getTimeTravel(); 35 | }, [origin, destination]); 36 | 37 | useEffect(() => { 38 | function navigateToRideScreen() { 39 | const hasTravelTimeInfo = (travelTimeInformation.totalSeconds > 0 && destination) 40 | if(hasTravelTimeInfo) { 41 | navigation.navigate('RideOptionsCardScreen'); 42 | } 43 | } 44 | navigateToRideScreen(); 45 | }, [travelTimeInformation]); 46 | 47 | 48 | return ( 49 | 62 | 67 | 68 | {origin?.placeName && ( 69 | 77 | 78 | )} 79 | 80 | {destination?.placeName && travelTimeInformation && ( 81 | 89 | 90 | 91 | )} 92 | 93 | 94 | ); 95 | } 96 | 97 | -------------------------------------------------------------------------------- /src/components/MapDirections/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { LatLng, Polyline } from 'react-native-maps'; 3 | import { DirectionsService } from '../../services/DirectionsService'; 4 | import { getPixelSize } from '../../utils/pixelsSize'; 5 | 6 | interface MapDirectionsProps { 7 | origin: any, 8 | destination: any, 9 | mapRef: any, 10 | } 11 | 12 | export const MapDirections = ({ origin, destination, mapRef }: MapDirectionsProps) => { 13 | 14 | const [directions, setDirections] = useState([]) 15 | 16 | useEffect(() => { 17 | if(!origin?.placeName || !destination?.placeName) { 18 | setDirections([]); 19 | mapRef.current.fitToSuppliedMarkers(['origin']) 20 | return; 21 | } 22 | 23 | DirectionsService 24 | .findDirections(origin, destination) 25 | .then((routes) => { 26 | setDirections(routes as LatLng[]) 27 | mapRef.current.fitToSuppliedMarkers(['origin', 'destination'], { 28 | edgePadding: { 29 | top: getPixelSize(110), 30 | right: getPixelSize(110), 31 | bottom: getPixelSize(110), 32 | left: getPixelSize(110) 33 | } 34 | }) 35 | }) 36 | }, [origin, destination]); 37 | 38 | if(!origin && !destination || !directions.length) { 39 | return null 40 | } 41 | 42 | return ( 43 | 44 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/components/NavFavourites/index.tsx: -------------------------------------------------------------------------------- 1 | import { View, FlatList, Text, StyleSheet, TouchableOpacity } from 'react-native'; 2 | import { Divider } from '../Divider'; 3 | import IconFeather from '@expo/vector-icons/Feather'; 4 | import tw from 'twrnc'; 5 | import { Coordinates } from '../../models/Coordinates'; 6 | 7 | interface NavFavouritesProps { 8 | onPress: (favourite: any) => void; 9 | } 10 | 11 | type icons = 'home' | 'briefcase'; 12 | 13 | const data = [ 14 | { 15 | id: "123", 16 | icon: "home" as icons, 17 | placeName: "Home", 18 | description: "Alameda Doutor Muricy, Centro, Curitiba - Paraná", 19 | location: { 20 | latitude: -25.4324938, 21 | longitude: -49.2721489, 22 | } as Coordinates 23 | }, 24 | { 25 | id: "456", 26 | icon: "briefcase" as icons, 27 | placeName: "Work", 28 | description: "Niterói CCR Barcas, Centro - Rio de Janeiro", 29 | location: { 30 | latitude: -22.8940922, 31 | longitude: -43.1239278, 32 | } as Coordinates 33 | } 34 | ]; 35 | 36 | export const NavFavourites = ({onPress}: NavFavouritesProps) => { 37 | return ( 38 | 39 | item.id.toString()} 41 | data={data} 42 | ItemSeparatorComponent={() => ( 43 | 44 | )} 45 | renderItem={({item}) => ( 46 | onPress({...item})} 48 | style={tw`flex-row items-center px-3 py-2`} 49 | > 50 | 57 | 58 | {item.placeName} 59 | {item.description} 60 | 61 | 62 | )} 63 | /> 64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/components/NavOptions/index.tsx: -------------------------------------------------------------------------------- 1 | import {FlatList, View, Text, TouchableOpacity, Image} from 'react-native'; 2 | import { useNavigation } from '@react-navigation/native'; 3 | import IconAntDesign from '@expo/vector-icons/AntDesign'; 4 | import tw from 'twrnc'; 5 | import { useDirectionsStore } from '../../stores/DirectionsStore'; 6 | 7 | // FIXME: CHANGE RESPONSABILITY CREATE TYPES 8 | // https://stackoverflow.com/questions/68779417/navigation-navigatehome-showing-some-error-in-typescript 9 | type Screens = 'MapScreen' | 'EatsScreen'; 10 | 11 | const data = [ 12 | { 13 | id: "123", 14 | title: "Get a ride", 15 | screen: "MapScreen" as Screens, 16 | image: "https://links.papareact.com/3pn" 17 | }, 18 | { 19 | id: "456", 20 | title: "Order a food", 21 | screen: "EatsScreen" as Screens, 22 | image: "https://links.papareact.com/28w" 23 | }, 24 | ] 25 | 26 | 27 | export const NavOptions = () => { 28 | const navigation = useNavigation() 29 | const origin = useDirectionsStore(state => state.origin); 30 | 31 | function hasOrigin(): boolean { 32 | return origin?.placeName ? true : false; 33 | } 34 | 35 | return ( 36 | item.id} 40 | renderItem={({ item }) => ( 41 | navigation.navigate(item.screen) } 43 | style={tw`m-2 p-2 pl-6 pb-8 pt-4 bg-gray-200 w-40 rounded`} 44 | disabled={!hasOrigin()} 45 | > 46 | 47 | 51 | {item.title} 52 | 56 | 57 | 58 | )} 59 | /> 60 | ); 61 | } -------------------------------------------------------------------------------- /src/components/PlaceAutoComplete/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react'; 2 | import { TouchableOpacity, View, TextInput, Text, FlatList, StyleProp, TextStyle, ViewStyle } from 'react-native'; 3 | import { debounce as debounceFn } from 'lodash'; 4 | import { GeocodingService } from '../../services/GeocodingService'; 5 | import { Coordinates } from '../../models/Coordinates'; 6 | import { Place } from '../../models/Place'; 7 | import IconFeather from '@expo/vector-icons/Feather'; 8 | import tw from 'twrnc'; 9 | 10 | 11 | interface PlacesAutoCompleteProps { 12 | onPress: (item: Place) => void, 13 | onSearchClear: () => void, 14 | placeholder: string, 15 | debounce?: number, 16 | containerStyle?: any, 17 | iconStyle?: any, 18 | inputStyle?: any, 19 | placesStyle?: any, 20 | userLocation?: Coordinates, 21 | } 22 | 23 | export const PlacesAutoComplete = ({ onPress, onSearchClear, placeholder, containerStyle, inputStyle, iconStyle, placesStyle, debounce = 700, userLocation }: PlacesAutoCompleteProps) => { 24 | const [places, setPlaces] = useState([]) 25 | const [search, setSearch] = useState('') 26 | const debounceFindPlaces = useCallback(debounceFn(findPlaces, debounce), []); 27 | 28 | function findPlaces(text: string) { 29 | if(!text.trim()) { 30 | handleClearSearch() 31 | return; 32 | } 33 | GeocodingService 34 | .findPlaces(text, userLocation ?? {latitude: 0, longitude: 0}) 35 | .then(places => setPlaces(places)) 36 | } 37 | 38 | function handleClearSearch() { 39 | setSearch('') 40 | setPlaces([]) 41 | onSearchClear(); 42 | } 43 | 44 | function onPlacePress(item: Place) { 45 | setSearch(item.placeName) 46 | setPlaces([]) 47 | onPress(item) 48 | } 49 | 50 | function onSearchChange(text: string) { 51 | setSearch(text) 52 | debounceFindPlaces(text) 53 | } 54 | 55 | 56 | return ( 57 | 58 | 59 | 66 | 73 | 74 | 75 | {places.length > 0 && ( 76 | item.id.toString()} 80 | renderItem={({item}) => ( 81 | onPlacePress(item)} 84 | > 85 | 86 | 87 | {item.placeName} 88 | 89 | 90 | 91 | )} 92 | /> 93 | )} 94 | 95 | 96 | ); 97 | } 98 | 99 | -------------------------------------------------------------------------------- /src/components/Separator/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | import tw from 'twrnc'; 4 | 5 | export const Separator = () => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/components/UberLogo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, Image} from 'react-native'; 3 | 4 | export const UberLogo = () => { 5 | return ( 6 | 10 | ); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/models/Coordinates.ts: -------------------------------------------------------------------------------- 1 | export type Coordinates = { 2 | latitude: number, 3 | longitude: number, 4 | } -------------------------------------------------------------------------------- /src/models/Direction.ts: -------------------------------------------------------------------------------- 1 | import { Coordinates } from './Coordinates'; 2 | 3 | export interface Direction { 4 | placeName: string, 5 | description: string, 6 | location: Coordinates, 7 | } -------------------------------------------------------------------------------- /src/models/MatrixDistance.ts: -------------------------------------------------------------------------------- 1 | import { Coordinates } from './Coordinates'; 2 | 3 | export interface MatrixDistance { 4 | code: string, 5 | travelTimeSeconds: number, 6 | travelTimeInformation: { 7 | hours: string, 8 | minutes: string, 9 | time: number, 10 | }, 11 | durations: Array>, 12 | sources: MatrixLocality[], 13 | destinations: MatrixLocality[], 14 | } 15 | 16 | export interface MatrixLocality { 17 | name: string, 18 | location: Coordinates, 19 | distance: number, 20 | } -------------------------------------------------------------------------------- /src/models/Place.ts: -------------------------------------------------------------------------------- 1 | import { Coordinates } from './Coordinates'; 2 | 3 | export interface Place { 4 | id: number, 5 | text: string, 6 | placeName: string, 7 | geometry: object, 8 | location: Coordinates 9 | } -------------------------------------------------------------------------------- /src/models/TravelTime.ts: -------------------------------------------------------------------------------- 1 | export interface TravelTime { 2 | hours: string, 3 | minutes: string, 4 | time: string, 5 | totalHours: number, 6 | totalMinutes: number, 7 | totalSeconds: number, 8 | } -------------------------------------------------------------------------------- /src/models/User.ts: -------------------------------------------------------------------------------- 1 | import { Coordinates } from "./Coordinates"; 2 | 3 | export interface User { 4 | name: string, 5 | phone?: string, 6 | email?: string, 7 | location: Coordinates 8 | } -------------------------------------------------------------------------------- /src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import 'react-native-gesture-handler'; 2 | import { NavigationContainer } from '@react-navigation/native'; 3 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 4 | 5 | import { ScreenOptions } from './options'; 6 | import { LoginScreen } from '../screens/LoginScreen'; 7 | import { HomeScreen } from '../screens/HomeScreen'; 8 | import { MapScreen } from '../screens/MapScreen'; 9 | import { EatsScreen } from '../screens/EatsScreen'; 10 | import { useUserStore } from '../stores/UserStore'; 11 | 12 | 13 | export type RootStackParamList = { 14 | HomeScreen: undefined; 15 | MapScreen: undefined; 16 | EatsScreen: undefined; 17 | NavigateCardScreen: undefined; 18 | RideOptionsCardScreen: undefined; 19 | Loginscreen: undefined; 20 | }; 21 | 22 | const Stack = createNativeStackNavigator(); 23 | 24 | export function Routes() { 25 | const user = useUserStore(state => state.user) 26 | 27 | return ( 28 | 29 | 30 | {user.name ? ( 31 | <> 32 | 33 | 34 | 35 | 36 | ) : ( 37 | <> 38 | 39 | 40 | )} 41 | 42 | 43 | ); 44 | } -------------------------------------------------------------------------------- /src/routes/options.ts: -------------------------------------------------------------------------------- 1 | export const ScreenOptions = { 2 | headerShown: false, 3 | } 4 | -------------------------------------------------------------------------------- /src/screens/EatsScreen.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | import { SafeAreaView } from 'react-native-safe-area-context'; 3 | 4 | export const EatsScreen = () => { 5 | return ( 6 | 7 | EatsScreen 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/screens/HomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | import { SafeAreaView } from 'react-native-safe-area-context'; 3 | import { useFocusEffect, useNavigation } from '@react-navigation/native'; 4 | import { useDirectionsStore } from '../stores/DirectionsStore'; 5 | import { useUserStore } from '../stores/UserStore'; 6 | import tw from 'twrnc'; 7 | 8 | import { Place } from '../models/Place'; 9 | import { UberLogo } from '../components/UberLogo'; 10 | import { PlacesAutoComplete } from '../components/PlaceAutoComplete'; 11 | import { NavOptions } from '../components/NavOptions'; 12 | import { NavFavourites } from '../components/NavFavourites'; 13 | 14 | 15 | export const HomeScreen = () => { 16 | const navigation = useNavigation(); 17 | 18 | const user = useUserStore(state => state.user); 19 | const origin = useDirectionsStore(state => state.origin) 20 | const destination = useDirectionsStore(state => state.destination) 21 | const setOrigin = useDirectionsStore(state => state.setOrigin) 22 | const setDestination = useDirectionsStore(state => state.setDestination) 23 | 24 | 25 | useFocusEffect(() => { 26 | resetInitialState(); 27 | }); 28 | 29 | function resetInitialState() { 30 | if(destination?.placeName) { 31 | setDestination(); 32 | } 33 | } 34 | 35 | function handlePressPlace(place: Place) { 36 | setOrigin({ 37 | placeName: place.placeName, 38 | description: place.placeName, 39 | location: place.location, 40 | }); 41 | } 42 | 43 | function handlePressFavourite(favourite: any) { 44 | setOrigin({ 45 | placeName: favourite.placeName, 46 | description: favourite.placeName, 47 | location: favourite.location, 48 | }); 49 | navigation.navigate('MapScreen'); 50 | } 51 | 52 | return ( 53 | 54 | 55 | 56 | handlePressPlace(place)} 59 | onSearchClear={() => setOrigin()} 60 | userLocation={user.location} 61 | /> 62 | 63 | 64 | 65 | 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /src/screens/LoginScreen.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { SafeAreaView, View, ImageBackground, TouchableOpacity, Text, TextInput } from 'react-native'; 3 | import * as Location from 'expo-location'; 4 | import { useUserStore } from '../stores/UserStore'; 5 | import { useNavigation } from '@react-navigation/native'; 6 | import tw from 'twrnc' 7 | 8 | // import loginBackground from '../../assets/login_background.png'; 9 | import { UberLogo } from '../components/UberLogo'; 10 | 11 | export const LoginScreen = () => { 12 | const navigation = useNavigation() 13 | const [name, setName] = useState('') 14 | const [phone, setPhone] = useState('') 15 | const [loading, setLoading] = useState(false) 16 | const setUser = useUserStore(state => state.setUser) 17 | 18 | async function handleClick() { 19 | try { 20 | setLoading(true); 21 | const { status } = await Location.requestForegroundPermissionsAsync(); 22 | if (status !== 'granted') { 23 | console.error('Permission to access location was denied'); 24 | return; 25 | } 26 | const { coords } = await Location.getCurrentPositionAsync({}); 27 | setUser({ 28 | name, 29 | phone, 30 | location: { 31 | latitude: coords.latitude, 32 | longitude: coords.longitude, 33 | } 34 | }); 35 | navigation.navigate('HomeScreen') 36 | } catch { 37 | setLoading(false) 38 | } 39 | } 40 | 41 | function disableSigninButton() { 42 | return loading || (!name || !phone) 43 | } 44 | 45 | return ( 46 | 47 | 48 | 49 | 50 | setName(value)} 54 | value={name} 55 | /> 56 | setPhone(value)} 61 | value={phone} 62 | /> 63 | 68 | 69 | {!loading ? 'Signin' : 'Loading...'} 70 | 71 | 72 | 73 | 74 | 75 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/screens/MapScreen.tsx: -------------------------------------------------------------------------------- 1 | import { SafeAreaView, View } from 'react-native'; 2 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 3 | import { ScreenOptions } from '../routes/options'; 4 | import tw from 'twrnc'; 5 | 6 | import { Map } from '../components/Map'; 7 | import { NavigateCardScreen } from './NavigateCardScreen'; 8 | import { RideOptionsCardScreen } from './RideOptionsCardScreen'; 9 | 10 | 11 | const Stack = createNativeStackNavigator() 12 | 13 | export const MapScreen = () => { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | {/* Nested Stack Navigation */} 21 | 22 | 27 | 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/screens/NavigateCardScreen.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigation } from '@react-navigation/native'; 2 | import { SafeAreaView, View, Text } from 'react-native'; 3 | import { useUserStore } from '../stores/UserStore'; 4 | import { useDirectionsStore } from '../stores/DirectionsStore'; 5 | import IconIonic from '@expo/vector-icons/Ionicons'; 6 | import tw from 'twrnc'; 7 | 8 | import { Chip } from '../components/Chip'; 9 | import { Separator } from '../components/Separator'; 10 | import { PlacesAutoComplete } from '../components/PlaceAutoComplete'; 11 | import { Place } from '../models/Place'; 12 | import { NavFavourites } from '../components/NavFavourites'; 13 | 14 | 15 | 16 | export const NavigateCardScreen = () => { 17 | const navigation = useNavigation() 18 | const user = useUserStore(state => state.user) 19 | const destination = useDirectionsStore(state => state.destination) 20 | const travelTime = useDirectionsStore(state => state.travelTimeInformation) 21 | const setDestination = useDirectionsStore(state => state.setDestination) 22 | 23 | function handlePressPlace(place: Place) { 24 | setDestination({ 25 | placeName: place.placeName, 26 | description: place.placeName, 27 | location: place.location, 28 | }) 29 | } 30 | 31 | function handlePressFavourite(favourite: any) { 32 | setDestination({ 33 | placeName: favourite.placeName, 34 | description: favourite.placeName, 35 | location: favourite.location, 36 | }) 37 | } 38 | 39 | function handleSearchClear() { 40 | setDestination(); 41 | } 42 | 43 | return ( 44 | 45 | 46 | 47 | Good Morning! {user.name} 48 | 49 | handlePressPlace(place)} 55 | onSearchClear={() => handleSearchClear()} 56 | userLocation={user.location} 57 | /> 58 | 59 | 60 | 61 | 62 | navigation.navigate('RideOptionsCardScreen')} 64 | text='Rides' 65 | disabled={!destination} 66 | bgColor='#222' 67 | textColor='#FFF' 68 | icon={} 69 | /> 70 | {}} 72 | text='Eats' 73 | disabled={!travelTime?.totalSeconds} 74 | bgColor='#eee' 75 | textColor='#333' 76 | icon={} 77 | /> 78 | 79 | 80 | ); 81 | } 82 | 83 | -------------------------------------------------------------------------------- /src/screens/RideOptionsCardScreen.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { SafeAreaView, View, TouchableOpacity, Image, Text, FlatList } from 'react-native'; 3 | import { useNavigation } from '@react-navigation/native'; 4 | import { useDirectionsStore } from '../stores/DirectionsStore'; 5 | import { TravelTime } from '../models/TravelTime'; 6 | import tw from 'twrnc'; 7 | import 'intl'; 8 | import 'intl/locale-data/jsonp/pt-BR'; 9 | 10 | import { Separator } from '../components/Separator'; 11 | import IconIonic from '@expo/vector-icons/Ionicons'; 12 | 13 | 14 | const SURGE_CHARGE_RATE = 0.5; 15 | const MIN_TRAVEL_PRICE = 4.50; 16 | const data = [ 17 | { 18 | id: 'Uber-X-123', 19 | title: 'UberX', 20 | multiplier: 1, 21 | image: 'https://links.papareact.com/3pn', 22 | }, 23 | { 24 | id: 'Uber-X-456', 25 | title: 'Uber Comfort', 26 | multiplier: 1.2, 27 | image: 'https://links.papareact.com/5w8', 28 | }, 29 | { 30 | id: 'Uber-LUX-789', 31 | title: 'Uber Black', 32 | multiplier: 1.75, 33 | image: 'https://links.papareact.com/7pf', 34 | } 35 | 36 | ] 37 | 38 | export const RideOptionsCardScreen = () => { 39 | const navigation = useNavigation(); 40 | const [seletedRide, setSelectedRide] = useState(null); 41 | const travelTime = useDirectionsStore(state => state.travelTimeInformation) 42 | 43 | function handleSelectRide(ride: any) { 44 | setSelectedRide(ride) 45 | } 46 | 47 | function displayTravelTime(travelTime: TravelTime): string { 48 | if(Number(travelTime?.totalHours) > 1) { 49 | return `${Number(travelTime?.totalHours)} hours ${Number(travelTime?.minutes)} minutes`; 50 | } 51 | return `${Number(travelTime?.totalMinutes) || 2} minutes`; 52 | } 53 | 54 | function calcTravelTimePrice(totalMinutes: number, multiplier: number): string { 55 | if(totalMinutes <= 0) totalMinutes = MIN_TRAVEL_PRICE; 56 | 57 | const price = new Intl.NumberFormat('pt-BR', { 58 | style: 'currency', 59 | currency: 'BRL' 60 | }).format((totalMinutes * SURGE_CHARGE_RATE * multiplier)); 61 | 62 | return price; 63 | } 64 | 65 | return ( 66 | 67 | 68 | 69 | navigation.navigate('NavigateCardScreen')} 72 | > 73 | 74 | 75 | Select a Ride 76 | 77 | 78 | {travelTime?.totalSeconds ? ( 79 | item.id.toString()} 82 | renderItem={({ item: {id, image, title, multiplier}, item }) => ( 83 | handleSelectRide(item)} 86 | > 87 | 91 | {travelTime?.totalSeconds && ( 92 | <> 93 | 94 | {title} 95 | {displayTravelTime(travelTime)} 96 | 97 | {calcTravelTimePrice(travelTime.totalMinutes, multiplier)} 98 | 99 | )} 100 | 101 | )} 102 | /> 103 | ) : ( 104 | Sorry! We can't find any Ride :( 105 | )} 106 | 107 | 108 | 109 | 113 | 114 | Confirm {seletedRide?.title} 115 | 116 | 117 | 118 | 119 | 120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /src/services/DirectionsService.ts: -------------------------------------------------------------------------------- 1 | import { directionsApi } from "../apis" 2 | import { Direction } from './../models/Direction'; 3 | import { Coordinates } from './../models/Coordinates'; 4 | 5 | export const DirectionsService = { 6 | 7 | async findDirections(origin: Direction, destination: Direction): Promise { 8 | const startCoords = `${origin.location.longitude}, ${origin.location.latitude}` 9 | const endCoords = `${destination.location.longitude}, ${destination.location.latitude}` 10 | 11 | const { data } = await directionsApi.get(`/driving-traffic/${startCoords};${endCoords}?alternatives=false&geometries=geojson&steps=false&overview=full`) 12 | 13 | const routes = data?.routes[0]?.geometry || [] 14 | 15 | if(!routes?.coordinates.length) return routes; 16 | 17 | return routes.coordinates.map((route: Array) => this._mapper(route)); 18 | }, 19 | 20 | _mapper(route: Array): Coordinates { 21 | return { 22 | latitude: route[1], 23 | longitude: route[0] 24 | } 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/services/GeocodingService.ts: -------------------------------------------------------------------------------- 1 | import { geocodingApi } from "../apis" 2 | import { Coordinates } from './../models/Coordinates'; 3 | import { Place } from './../models/Place'; 4 | 5 | 6 | export const GeocodingService = { 7 | 8 | async findPlaces(search: string, location: Coordinates): Promise { 9 | const userLocation = `${location.longitude},${location.latitude}`; //'-49.273182,-25.4354423' 10 | const { data } = await geocodingApi.get(`/mapbox.places/${search}.json?country=BR&limit=5&autocomplete=true&proximity=${userLocation}`) 11 | return data.features.map((data: any) => this._mapper(data)) 12 | }, 13 | 14 | _mapper(data: any): Place { 15 | return { 16 | id: data.id, 17 | text: data.text, 18 | placeName: data.place_name, 19 | geometry: data.geometry, 20 | location: { 21 | latitude: data.geometry.coordinates[1], 22 | longitude: data.geometry.coordinates[0], 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/services/MatrixService.ts: -------------------------------------------------------------------------------- 1 | import { matrixApi } from "../apis" 2 | import { calcSecondsToHours } from "../utils/timeCalculator"; 3 | import { Direction } from './../models/Direction'; 4 | import { MatrixDistance } from './../models/MatrixDistance'; 5 | 6 | export const MatrixService = { 7 | 8 | async findMatrixDuration(origin: Direction, destination: Direction): Promise { 9 | const startCoords = `${origin.location.longitude},${origin.location.latitude}` 10 | const endCoords = `${destination.location.longitude},${destination.location.latitude}` 11 | 12 | const { data } = await matrixApi.get(`/mapbox/driving/${startCoords};${endCoords}`) 13 | 14 | return this._mapper(data) 15 | }, 16 | 17 | _mapper(data: any): MatrixDistance { 18 | const sources = data?.sources.map((source: any) => ({ 19 | ...source, 20 | location: { 21 | latitude: source?.location[1], 22 | longitude: source?.location[0], 23 | } 24 | })); 25 | 26 | const destinations = data?.sources.map((destination: any) => ({ 27 | ...destination, 28 | location: { 29 | latitude: destination?.location[1], 30 | longitude: destination?.location[0], 31 | } 32 | })) 33 | 34 | const travelTimeSeconds = data.durations.reduce((travel: number, times: Array) => { 35 | const seconds = times.reduce((acc, seconds) => acc + seconds); 36 | return travel + seconds; 37 | }, 0); 38 | 39 | const travelTimeInformation = calcSecondsToHours(travelTimeSeconds) 40 | 41 | return { 42 | ...data, 43 | sources, 44 | travelTimeSeconds, 45 | travelTimeInformation, 46 | destinations, 47 | 48 | } 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/stores/DirectionsStore.ts: -------------------------------------------------------------------------------- 1 | import { TravelTime } from './../models/TravelTime'; 2 | import create from "zustand"; 3 | 4 | import { Direction } from '../models/Direction'; 5 | 6 | interface State { 7 | origin?: Direction | null, 8 | destination?: Direction | null, 9 | travelTimeInformation?: TravelTime, 10 | 11 | setOrigin: (origin?: Direction) => void, 12 | setDestination: (destination?: Direction) => void, 13 | setTravelTimeInformation: (travelTimeInformation?: TravelTime) => void, 14 | } 15 | 16 | 17 | export const useDirectionsStore = create((set) => ({ 18 | origin: {} as Direction, 19 | destination: {} as Direction, 20 | travelTimeInformation: {} as TravelTime, 21 | 22 | setOrigin: (origin) => set({ origin }), 23 | setDestination: (destination) => set({ destination }), 24 | setTravelTimeInformation: (travelTimeInformation) => set({ travelTimeInformation }), 25 | })); -------------------------------------------------------------------------------- /src/stores/UserStore.ts: -------------------------------------------------------------------------------- 1 | import { User } from './../models/User'; 2 | import create from "zustand"; 3 | 4 | 5 | interface State { 6 | user: User, 7 | setUser: (user: User) => void 8 | } 9 | 10 | export const useUserStore = create((set) => ({ 11 | user: {} as User, 12 | setUser: (user) => set({ user }), 13 | })); -------------------------------------------------------------------------------- /src/utils/ellipsisText.ts: -------------------------------------------------------------------------------- 1 | 2 | export function ellipsisText(text: string, limit: number) { 3 | if(!text.length) return ''; 4 | 5 | return text.length > limit ? `${text.substring(0, limit - 3)}...` : text; 6 | } -------------------------------------------------------------------------------- /src/utils/pixelsSize.ts: -------------------------------------------------------------------------------- 1 | import { Platform, PixelRatio } from 'react-native'; 2 | 3 | export function getPixelSize(pixels: number) { 4 | return Platform.select({ 5 | ios: pixels, 6 | android: PixelRatio.getPixelSizeForLayoutSize(pixels) 7 | }) 8 | } -------------------------------------------------------------------------------- /src/utils/timeCalculator.ts: -------------------------------------------------------------------------------- 1 | export function calcSecondsToHours(seconds: number = 0) { 2 | const time = new Date(seconds * 1000).toISOString().slice(11, 16); 3 | 4 | return { 5 | hours: time.slice(0,2), 6 | minutes: time.slice(3), 7 | totalMinutes: Math.round(seconds / 60), 8 | totalHours: Math.floor(seconds / 3600), 9 | totalSeconds: seconds, 10 | time: time, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | --------------------------------------------------------------------------------