├── .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 |

28 |

29 |

30 |

31 |

32 |

33 |
34 |
35 |
36 |
37 |
38 |

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 |
--------------------------------------------------------------------------------