├── .eslintrc.js ├── .expo-shared ├── README.md └── assets.json ├── .gitignore ├── .prettierrc ├── App.tsx ├── README.md ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── doc └── demo.gif ├── package-lock.json ├── package.json ├── src ├── @types │ ├── dto │ │ ├── location.ts │ │ └── weather.ts │ ├── global.d.ts │ └── store │ │ └── app.state.ts ├── assets │ ├── animations │ │ └── empty.json │ └── videos │ │ └── church.mp4 ├── components │ ├── Footer │ │ ├── index.tsx │ │ └── styles.ts │ ├── Header │ │ ├── index.tsx │ │ └── styles.ts │ ├── Temperature │ │ ├── index.tsx │ │ └── styles.ts │ ├── WeatherCondition │ │ └── index.tsx │ ├── WeatherForecastItem │ │ ├── index.tsx │ │ └── styles.ts │ └── WeatherSummaryItem │ │ ├── index.tsx │ │ └── styles.ts ├── config │ ├── constants.ts │ ├── index.ts │ └── reactotron.ts ├── navigation │ ├── NavigationService.ts │ └── routes.tsx ├── screens │ ├── Home │ │ ├── index.tsx │ │ └── styles.ts │ └── Places │ │ ├── index.tsx │ │ └── styles.ts ├── services │ └── api │ │ ├── client │ │ ├── errorHandler.ts │ │ └── index.ts │ │ └── resources │ │ ├── WeatherService.ts │ │ └── index.ts ├── store │ ├── ducks │ │ ├── Location │ │ │ ├── ChooseLocation.ts │ │ │ └── NewLocation.ts │ │ ├── Weather │ │ │ ├── GetCurrentWeather.ts │ │ │ └── GetForecastWeather.ts │ │ └── index.ts │ ├── index.ts │ └── sagas │ │ ├── Location │ │ ├── ChooseLocation.ts │ │ └── NewLocation.ts │ │ ├── Weather │ │ ├── GetCurrentWeather.ts │ │ └── GetForecastWeather.ts │ │ └── index.ts ├── theme │ └── colors.ts └── utils │ ├── dateFormatUtil.ts │ └── stringFormatUtil.ts ├── tsconfig.json ├── yarn-error.log └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | }, 5 | globals: { 6 | __DEV__: true, 7 | }, 8 | extends: ['plugin:react/recommended', 'airbnb'], 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | ecmaFeatures: { 12 | jsx: true, 13 | }, 14 | ecmaVersion: 13, 15 | sourceType: 'module', 16 | }, 17 | plugins: ['react', '@typescript-eslint', 'prettier'], 18 | rules: { 19 | 'react/jsx-filename-extension': [ 20 | 'warn', 21 | { extensions: ['.jsx', '.js', '.tsx'] }, 22 | ], 23 | 'import/prefer-default-export': 'off', 24 | 'no-param-reassign': 'off', 25 | 'no-console': ['error', { allow: ['tron'] }], 26 | 'no-use-before-define': 'off', 27 | 'import/extensions': 'off', 28 | 'import/no-unresolved': 'off', 29 | }, 30 | settings: { 31 | 'import/resolver': { 32 | node: { 33 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 34 | }, 35 | typescript: {}, 36 | 'babel-plugin-root-import': { 37 | rootPathPrefix: '~', 38 | rootPathSuffix: 'src', 39 | }, 40 | }, 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /.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 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | 17 | # env 18 | .env -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "jsxBracketSameLine": true, 4 | "singleQuote": true, 5 | "trailingComma": "es5", 6 | "endOfLine": "auto" 7 | } -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import '~/config/reactotron'; 2 | import React from 'react'; 3 | 4 | import { Provider } from 'react-redux'; 5 | import { PersistGate } from 'redux-persist/integration/react'; 6 | 7 | import Routes from '~/navigation/routes'; 8 | 9 | import { persistor, store } from '~/store'; 10 | 11 | export default function App() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Weather App 2 | 3 |

4 | App demonstration 5 |

6 | 7 | >🚀 🌧 Weather app in react native using typescript. 8 | 9 | ## 💻 Prerequisites 10 | 11 | * Expo 12 | * NodeJS 13 | 14 | 15 | ## Config 16 | 17 | Edit ``constants`` file in ``src/config`` and set this config variables. 18 | 19 | ```typescript 20 | const OPEN_WEATHER_API_APP_ID = ''; 21 | const GOOGLE_PLACES_API_KEY = ''; 22 | ``` 23 | 24 | 25 | ## 🚀 Install 26 | 27 | ``` 28 | yarn install 29 | or 30 | npm install 31 | ``` 32 | 33 | ``` 34 | expo start 35 | ``` 36 | 37 | ## Inspiration 38 | 39 | This application is an implementation [Sang Nguyen's](https://dribbble.com/sanggggg) layout available [here](https://dribbble.com/shots/16307033--28-Weather-App-Interaction) 40 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Weather app", 4 | "slug": "weather-app", 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 | "bundleIdentifier": "com.wazowsky.weather-app" 22 | }, 23 | "android": { 24 | "adaptiveIcon": { 25 | "foregroundImage": "./assets/adaptive-icon.png", 26 | "backgroundColor": "#FFFFFF" 27 | } 28 | }, 29 | "web": { 30 | "favicon": "./assets/favicon.png" 31 | }, 32 | "description": "", 33 | "githubUrl": "https://github.com/mateuschaves/weather-app" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mateuschaves/weather-react-native/b2e2f2fd81075c507c6c553f372cec04ab369535/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mateuschaves/weather-react-native/b2e2f2fd81075c507c6c553f372cec04ab369535/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mateuschaves/weather-react-native/b2e2f2fd81075c507c6c553f372cec04ab369535/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mateuschaves/weather-react-native/b2e2f2fd81075c507c6c553f372cec04ab369535/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 | [ 7 | 'babel-plugin-root-import', 8 | { 9 | rootPathPrefix: '~', 10 | rootPathSuffix: 'src', 11 | }, 12 | ], 13 | ], 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /doc/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mateuschaves/weather-react-native/b2e2f2fd81075c507c6c553f372cec04ab369535/doc/demo.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "@react-native-async-storage/async-storage": "~1.15.0", 12 | "@react-navigation/native": "^6.0.6", 13 | "@react-navigation/native-stack": "^6.2.5", 14 | "axios": "^0.28.0", 15 | "expo": "~50.0.15", 16 | "expo-av": "~13.10.5", 17 | "expo-status-bar": "~1.1.0", 18 | "lodash": "^4.17.21", 19 | "lottie-ios": "3.2.3", 20 | "lottie-react-native": "^4.1.3", 21 | "react": "17.0.1", 22 | "react-content-loader": "^6.0.3", 23 | "react-dom": "17.0.1", 24 | "react-native": "0.73.6", 25 | "react-native-animated-numbers": "^0.5.0", 26 | "react-native-config": "^1.4.5", 27 | "react-native-google-places-autocomplete": "^2.4.1", 28 | "react-native-pager-view": "5.4.6", 29 | "react-native-safe-area-context": "^3.3.2", 30 | "react-native-screens": "~3.8.0", 31 | "react-native-swiper": "^1.6.0", 32 | "react-native-tab-view": "^3.1.1", 33 | "react-native-web": "0.17.1", 34 | "react-native-wizard": "^2.1.0", 35 | "react-redux": "^7.2.6", 36 | "reactotron-react-native": "^5.0.0", 37 | "reactotron-redux": "^3.1.3", 38 | "reactotron-redux-saga": "^4.2.3", 39 | "redux": "^4.1.1", 40 | "redux-persist": "^6.0.0", 41 | "redux-saga": "^1.1.3", 42 | "styled-components": "^5.3.3" 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.12.9", 46 | "@types/axios": "^0.14.0", 47 | "@types/lodash": "^4.14.176", 48 | "@types/react": "~17.0.21", 49 | "@types/react-native": "~0.64.12", 50 | "@types/styled-components": "^5.1.15", 51 | "@types/styled-components-react-native": "^5.1.2", 52 | "@typescript-eslint/eslint-plugin": "^5.2.0", 53 | "@typescript-eslint/parser": "^5.2.0", 54 | "babel-eslint": "^10.1.0", 55 | "babel-plugin-root-import": "^6.6.0", 56 | "eslint": "^7.32.0", 57 | "eslint-config-airbnb": "^18.2.1", 58 | "eslint-config-prettier": "^8.3.0", 59 | "eslint-import-resolver-babel-plugin-root-import": "^1.1.1", 60 | "eslint-plugin-import": "^2.25.2", 61 | "eslint-plugin-jsx-a11y": "^6.4.1", 62 | "eslint-plugin-prettier": "^4.0.0", 63 | "eslint-plugin-react": "^7.26.1", 64 | "eslint-plugin-react-hooks": "^4.2.0", 65 | "prettier": "^2.4.1", 66 | "typescript": "~4.3.5" 67 | }, 68 | "private": true 69 | } 70 | -------------------------------------------------------------------------------- /src/@types/dto/location.ts: -------------------------------------------------------------------------------- 1 | import { GooglePlaceDetail } from 'react-native-google-places-autocomplete'; 2 | 3 | /* eslint-disable camelcase */ 4 | export interface LocationDto { 5 | message: string; 6 | } 7 | 8 | export interface LocationResponseItem { 9 | 10 | } 11 | 12 | export interface Location extends GooglePlaceDetail{} 13 | -------------------------------------------------------------------------------- /src/@types/dto/weather.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | export interface WeatherByCoordinatesDto { 3 | latitude: number; 4 | longitude: number; 5 | } 6 | 7 | export interface ForecastWeatherByCoordinatesDto extends WeatherByCoordinatesDto { 8 | exclude: string; 9 | } 10 | 11 | export interface CurrentWeather { 12 | coord: { 13 | lat: number; 14 | lon: number; 15 | }, 16 | weather: [{ 17 | id: number; 18 | main: string; 19 | description: string; 20 | icon: string; 21 | }], 22 | base: string; 23 | main: { 24 | temp: number; 25 | feels_like: number; 26 | temp_min: number; 27 | temp_max: number; 28 | pressure: number; 29 | humidity: number; 30 | }, 31 | wind: { 32 | speed: number; 33 | deg: number; 34 | }, 35 | id: number; 36 | name: string; 37 | cod: number; 38 | } 39 | 40 | export interface ForecastWeather { 41 | daily: [{ 42 | dt: number; 43 | temp: { 44 | max: number; 45 | min: number; 46 | }, 47 | weather: [{ 48 | id: number; 49 | main: string; 50 | description: string; 51 | icon: string; 52 | }] 53 | }] 54 | } 55 | -------------------------------------------------------------------------------- /src/@types/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Console { 2 | tron: any; 3 | } 4 | -------------------------------------------------------------------------------- /src/@types/store/app.state.ts: -------------------------------------------------------------------------------- 1 | import { Location } from '~/@types/dto/location'; 2 | import { CurrentWeather, ForecastWeather } from '~/@types/dto/weather'; 3 | 4 | export interface InitialChooseLocationStateProps { 5 | location: Location | {}; 6 | loading: boolean; 7 | error: any; 8 | } 9 | 10 | export interface InitialNewLocationStateProps { 11 | location: [Location] | []; 12 | loading: boolean; 13 | error: any; 14 | } 15 | 16 | export interface InitialGetCurrentWeatherStateProps { 17 | weather: CurrentWeather | undefined; 18 | loading: boolean; 19 | error: any; 20 | } 21 | 22 | export interface InitialForecastWeatherStateProps { 23 | forecastWeather: ForecastWeather | undefined; 24 | loading: boolean; 25 | error: any; 26 | } 27 | 28 | export interface RootState { 29 | locations: InitialNewLocationStateProps, 30 | choosedLocation: InitialChooseLocationStateProps, 31 | currentWeather: InitialGetCurrentWeatherStateProps, 32 | forecastWeather: InitialForecastWeatherStateProps, 33 | } 34 | -------------------------------------------------------------------------------- /src/assets/animations/empty.json: -------------------------------------------------------------------------------- 1 | {"v":"5.1.1","fr":60,"ip":0,"op":180,"w":256,"h":256,"nm":"PartlyCloudyDay","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"cloud","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":0,"s":[76.83,63.21,0],"e":[78.83,63.21,0],"to":[0.33333334326744,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":45,"s":[78.83,63.21,0],"e":[76.83,63.21,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":90,"s":[76.83,63.21,0],"e":[78.83,63.21,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":135,"s":[78.83,63.21,0],"e":[76.83,63.21,0],"to":[0,0,0],"ti":[0.33333334326744,0,0]},{"t":180}],"ix":2},"a":{"a":0,"k":[45.49,27.05,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,10.5],[-10.88,0],[-2.72,-1.37],[-12.29,0],[0,-14.94],[0.01,-0.3],[0,-5.3],[7.96,0],[0,0],[0,0],[0,0]],"o":[[0,0],[-10.88,0],[0,-10.5],[3.29,0],[3.79,-10.61],[15.49,0],[0,0.31],[4.52,2.35],[0,7.67],[0,0],[-0.05,0],[0,0],[0,0]],"v":[[55.05,54.1],[19.71,54.1],[0,35.09],[19.71,16.08],[28.82,18.23],[55.35,0],[83.4,27.05],[83.38,27.96],[90.98,40.21],[76.57,54.1],[55.35,54.1],[55.05,54.1],[55.35,54.1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":7,"ix":5},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.909803986549,0.909803986549,0.909803986549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"cloud","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":0,"s":[50.49,183.99,0],"e":[53.49,183.99,0],"to":[0.5,0,0],"ti":[-3.56038412974158e-7,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":40,"s":[53.49,183.99,0],"e":[50.49,183.99,0],"to":[3.56038412974158e-7,0,0],"ti":[-3.56038412974158e-7,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":85,"s":[50.49,183.99,0],"e":[53.49,183.99,0],"to":[3.56038412974158e-7,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":130,"s":[53.49,183.99,0],"e":[50.49,183.99,0],"to":[0,0,0],"ti":[0.5,0,0]},{"t":180}],"ix":2},"a":{"a":0,"k":[45.49,27.05,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,10.5],[-10.88,0],[-2.72,-1.37],[-12.29,0],[0,-14.94],[0.01,-0.3],[0,-5.3],[7.96,0],[0,0],[0,0],[0,0]],"o":[[0,0],[-10.88,0],[0,-10.5],[3.29,0],[3.79,-10.61],[15.49,0],[0,0.31],[4.52,2.35],[0,7.67],[0,0],[-0.05,0],[0,0],[0,0]],"v":[[55.05,54.1],[19.71,54.1],[0,35.09],[19.71,16.08],[28.82,18.23],[55.35,0],[83.4,27.05],[83.38,27.96],[90.98,40.21],[76.57,54.1],[55.35,54.1],[55.05,54.1],[55.35,54.1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":7,"ix":5},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.909803986549,0.909803986549,0.909803986549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"cloud","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":0,"s":[182.765,116.19,0],"e":[186.765,116.19,0],"to":[0.66666668653488,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":50,"s":[186.765,116.19,0],"e":[182.765,116.19,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":95,"s":[182.765,116.19,0],"e":[186.765,116.19,0],"to":[0,0,0],"ti":[-1.01725255774454e-7,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":140,"s":[186.765,116.19,0],"e":[182.765,116.19,0],"to":[1.01725255774454e-7,0,0],"ti":[0.66666656732559,0,0]},{"t":180}],"ix":2},"a":{"a":0,"k":[60.235,35.82,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,13.9],[-14.41,0],[-3.61,-1.82],[-16.28,0],[0,-19.78],[0.01,-0.4],[0,-7.01],[10.53,0],[0,0],[0,0],[0,0]],"o":[[0,0],[-14.41,0],[0,-13.9],[4.35,0],[5.02,-14.05],[20.51,0],[0,0.4],[5.99,3.1],[0,10.16],[0,0],[-0.06,0],[0,0],[0,0]],"v":[[72.9,71.64],[26.1,71.64],[0,46.47],[26.1,21.3],[38.16,24.14],[73.29,0],[110.43,35.82],[110.41,37.03],[120.47,53.24],[101.4,71.64],[73.29,71.64],[72.9,71.64],[73.29,71.64]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":7,"ix":5},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.909803986549,0.909803986549,0.909803986549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"sun","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[129.275,127.375,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[170.59,169.85],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.788235008717,0.188234999776,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"sun","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"bond","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[127,128,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[256,256],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"bond","np":1,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /src/assets/videos/church.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mateuschaves/weather-react-native/b2e2f2fd81075c507c6c553f372cec04ab369535/src/assets/videos/church.mp4 -------------------------------------------------------------------------------- /src/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import { ScrollView, useWindowDimensions } from 'react-native'; 4 | import { TabBar, SceneMap } from 'react-native-tab-view'; 5 | 6 | import { useDispatch, useSelector } from 'react-redux'; 7 | import _ from 'lodash'; 8 | import { getForecastWeatherActions } from '../../store/ducks/Weather/GetForecastWeather'; 9 | import { InitialGetCurrentWeatherStateProps, RootState } from '~/@types/store/app.state'; 10 | import { InitialForecastWeatherStateProps } from '../../@types/store/app.state'; 11 | 12 | import { capitalizeFirstLetter, formatTemperature } from '~/utils/stringFormatUtil'; 13 | 14 | import WeatherForecastItem from '../WeatherForecastItem'; 15 | import WeatherSummaryItem from '../WeatherSummaryItem'; 16 | 17 | import { 18 | Container, FooterTitle, WeatherTab, WeatherSummary, 19 | } from './styles'; 20 | import { getDayLabel } from '~/utils/dateFormatUtil'; 21 | 22 | export default function Footer() { 23 | const layout = useWindowDimensions(); 24 | const [index, setIndex] = useState(0); 25 | const dispatch = useDispatch(); 26 | 27 | const weather = useSelector( 28 | (state) => state.currentWeather, 29 | ); 30 | const forecastWeather = useSelector( 31 | (state) => state.forecastWeather, 32 | ); 33 | 34 | const feelsLike = _.get(weather, 'weather.main.feels_like', 0); 35 | const windSpeed = _.get(weather, 'weather.wind.speed', 0); 36 | const pressure = _.get(weather, 'weather.main.pressure', 0); 37 | const humidity = _.get(weather, 'weather.main.humidity', 0); 38 | 39 | const lat = _.get(weather, 'weather.coord.lat', 0); 40 | const lon = _.get(weather, 'weather.coord.lon', 0); 41 | 42 | useEffect(() => { 43 | if (index) { 44 | dispatch( 45 | getForecastWeatherActions.getForecastWeather({ latitude: lat, longitude: lon, exclude: 'hourly,minutely' }), 46 | ); 47 | } 48 | }, [index]); 49 | 50 | const [routes] = useState([ 51 | { key: 'now', title: 'Agora' }, 52 | { key: 'daily', title: 'Diariamente' }, 53 | ]); 54 | 55 | const WeatherSummaryComponent = () => ( 56 | 57 | 62 | 67 | 72 | 77 | 78 | ); 79 | 80 | const WeatherForecastComponent = () => ( 81 | 82 | {forecastWeather.forecastWeather?.daily.slice(0, 6).map((day, index) => ( 83 | 91 | ))} 92 | 93 | ); 94 | 95 | const renderTabBar = (props: any) => ( 96 | 104 | ); 105 | 106 | const renderScene = SceneMap({ 107 | now: WeatherSummaryComponent, 108 | daily: WeatherForecastComponent, 109 | }); 110 | 111 | return ( 112 | 113 | 114 | Tempo 115 | {index === 0 ? ' agora' : ' nos próximos 7 dias'} 116 | 117 | 134 | 135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /src/components/Footer/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | import { Platform } from 'react-native'; 3 | import { initialWindowMetrics } from 'react-native-safe-area-context'; 4 | 5 | // eslint-disable-next-line import/no-extraneous-dependencies 6 | import { TabView } from 'react-native-tab-view'; 7 | import colors from '~/theme/colors'; 8 | 9 | const paddingBottom = Platform.OS === 'ios' ? (initialWindowMetrics?.insets?.bottom || 0) + 10 : 0; 10 | 11 | export const WeatherSummary = styled.View` 12 | flex-direction: row; 13 | flex-wrap: wrap; 14 | justify-content: space-evenly; 15 | align-items: center; 16 | margin-left: 30px; 17 | `; 18 | 19 | export const Container = styled.View` 20 | background-color: ${colors.white}; 21 | border-radius: 16px; 22 | padding-top: 16px; 23 | height: 35%; 24 | padding-bottom: ${paddingBottom}px; 25 | `; 26 | 27 | export const FooterTitle = styled.Text` 28 | font-size: 20px; 29 | font-weight: 600; 30 | margin-bottom: 16px; 31 | margin-left: 16px; 32 | text-align: left; 33 | align-items: center; 34 | `; 35 | 36 | export const WeatherTab = styled(TabView)` 37 | background-color: ${colors.white}; 38 | `; 39 | -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // eslint-disable-next-line import/no-extraneous-dependencies 4 | import { Ionicons } from '@expo/vector-icons'; 5 | 6 | import { 7 | Container, CityInfoContainer, City, Date, 8 | } from './styles'; 9 | 10 | interface HeaderProps { 11 | city: string; 12 | date: string; 13 | handleAddPlaceClick: () => void, 14 | } 15 | 16 | export default function Header({ city, date, handleAddPlaceClick }: HeaderProps) { 17 | return ( 18 | 19 | 20 | {city} 21 | {date} 22 | 23 | 24 | handleAddPlaceClick()} /> 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Header/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Container = styled.View` 4 | flex-direction: row; 5 | justify-content: space-between; 6 | align-items: center; 7 | margin-bottom: 45px; 8 | margin-top: 20px; 9 | `; 10 | 11 | export const CityInfoContainer = styled.View` 12 | flex-direction: column; 13 | `; 14 | 15 | export const City = styled.Text` 16 | font-weight: bold; 17 | font-size: 20px; 18 | `; 19 | 20 | export const Date = styled.Text` 21 | font-size: 17px; 22 | margin-top: 4px; 23 | `; 24 | -------------------------------------------------------------------------------- /src/components/Temperature/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | TemperatureInfo, 5 | TemperatureDescriptionContainer, 6 | TemperatureDescription, 7 | TemperatureContainer, 8 | TemperatureUnit, 9 | Value, 10 | WeatherIcon, 11 | } from './styles'; 12 | 13 | interface TemperatureProps { 14 | temperatureDescription: string; 15 | temperature: number; 16 | temperatureUnit: string; 17 | iconUri: string; 18 | } 19 | 20 | export default function Temperature({ 21 | temperatureDescription, 22 | temperature, 23 | temperatureUnit, 24 | iconUri, 25 | }: TemperatureProps) { 26 | return ( 27 | 28 | 29 | 30 | {temperatureDescription} 31 | 32 | 33 | 41 | {temperatureUnit} 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/components/Temperature/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | import AnimatedNumbers from 'react-native-animated-numbers'; 3 | import colors from '~/theme/colors'; 4 | 5 | export const Value = styled(AnimatedNumbers)` 6 | color: ${colors.black}; 7 | font-size: 90px; 8 | font-weight: 600; 9 | `; 10 | 11 | export const TemperatureUnit = styled.Text` 12 | color: ${colors.black}; 13 | font-size: 90px; 14 | font-weight: 600; 15 | margin: 0; 16 | `; 17 | 18 | export const TemperatureInfo = styled.View` 19 | flex-direction: row; 20 | justify-content: space-between; 21 | `; 22 | 23 | export const TemperatureDescription = styled.Text` 24 | font-size: 20px; 25 | font-weight: 600; 26 | `; 27 | 28 | export const TemperatureDescriptionContainer = styled.View` 29 | flex-direction: column; 30 | width: 100px; 31 | `; 32 | 33 | export const TemperatureContainer = styled.View` 34 | flex-direction: row; 35 | `; 36 | 37 | export const WeatherIcon = styled.Image` 38 | width: 70px; 39 | height: 50px; 40 | `; 41 | -------------------------------------------------------------------------------- /src/components/WeatherCondition/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import _ from 'lodash'; 4 | 5 | import { InitialGetCurrentWeatherStateProps, RootState } from '~/@types/store/app.state'; 6 | 7 | import Header from '~/components/Header'; 8 | import Temperature from '~/components/Temperature'; 9 | import { capitalizeFirstLetter } from '~/utils/stringFormatUtil'; 10 | import { formatToDateString } from '~/utils/dateFormatUtil'; 11 | 12 | interface WeatherConditionProps { 13 | city: string; 14 | handleAddPlaceClick: () => void; 15 | temperature: number; 16 | temperatureDescription: string; 17 | } 18 | 19 | export default function WeatherCondition({ 20 | city, 21 | temperature, 22 | temperatureDescription, 23 | handleAddPlaceClick, 24 | }: WeatherConditionProps) { 25 | const { weather } = useSelector( 26 | (state) => state.currentWeather, 27 | ); 28 | 29 | return ( 30 | <> 31 |
36 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/WeatherForecastItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // eslint-disable-next-line import/no-extraneous-dependencies 4 | import { Ionicons } from '@expo/vector-icons'; 5 | 6 | import { 7 | WeatherContainerItem, 8 | WeatherConditionTitle, 9 | WeatherDayForecast, 10 | WeatherIcon, 11 | WeatherMaxAndMin, 12 | } from './styles'; 13 | 14 | interface WeatherForecastItemProps { 15 | day: string; 16 | iconUri: string; 17 | conditionTitle: string; 18 | max: string; 19 | min: string; 20 | } 21 | 22 | export default function WeatherForecastItem({ day, iconUri, conditionTitle, max, min }: WeatherForecastItemProps) { 23 | return ( 24 | 25 | 26 | {day} 27 | 28 | 29 | 30 | {conditionTitle} 31 | 32 | 33 | 34 | 35 | {max} 36 | 37 | {min} 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/WeatherForecastItem/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | import colors from '~/theme/colors'; 3 | 4 | export const WeatherMaxAndMin = styled.View` 5 | flex: 1; 6 | flex-direction: row; 7 | align-items: center; 8 | `; 9 | 10 | export const WeatherContainerItem = styled.View` 11 | flex: 1; 12 | flex-direction: row; 13 | padding: 0 16px 0 16px; 14 | align-items: center; 15 | background-color: ${colors.bacgkroundLightColor}; 16 | border-radius: 8px; 17 | margin-top: 8px; 18 | `; 19 | 20 | export const WeatherConditionTitle = styled.Text` 21 | flex: 1; 22 | font-size: 20px; 23 | font-weight: 400; 24 | `; 25 | 26 | export const WeatherDayForecast = styled.Text` 27 | flex: 0.4; 28 | font-size: 20px; 29 | font-weight: 700; 30 | `; 31 | 32 | export const WeatherIcon = styled.Image` 33 | flex: 0.2; 34 | width: 50px; 35 | height: 50px; 36 | margin-right: 16px; 37 | margin-left: 16px; 38 | `; 39 | -------------------------------------------------------------------------------- /src/components/WeatherSummaryItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // eslint-disable-next-line import/no-extraneous-dependencies 4 | import { Ionicons } from '@expo/vector-icons'; 5 | 6 | import { 7 | Container, WeatherContainer, WeatherIconContainer, WeatherLabel, WeatherValue, 8 | } from './styles'; 9 | 10 | interface WeatherSummaryItemProps { 11 | weatherLabel: string; 12 | weatherValue: string; 13 | iconName: string; 14 | } 15 | 16 | export default function WeatherSummaryItem({iconName, weatherLabel, weatherValue}: WeatherSummaryItemProps) { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | {weatherLabel} 25 | 26 | 27 | {weatherValue} 28 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/WeatherSummaryItem/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | import colors from '~/theme/colors'; 3 | 4 | export const Container = styled.View` 5 | flex-direction: row; 6 | align-items: center; 7 | min-width: 40%; 8 | margin-bottom: 8px; 9 | justify-content: flex-start; 10 | `; 11 | 12 | export const WeatherLabel = styled.Text` 13 | font-weight: 600; 14 | color: ${colors.labelColor}; 15 | margin-bottom: 4px; 16 | `; 17 | 18 | export const WeatherValue = styled.Text` 19 | font-weight: 700; 20 | `; 21 | 22 | export const WeatherContainer = styled.View``; 23 | 24 | export const WeatherIconContainer = styled.View` 25 | justify-content: center; 26 | align-items: center; 27 | background-color: ${colors.bacgkroundLightColor}; 28 | border-radius: 90px; 29 | width: 45px; 30 | height: 45px; 31 | margin: 8px; 32 | `; 33 | -------------------------------------------------------------------------------- /src/config/constants.ts: -------------------------------------------------------------------------------- 1 | const BASE_URL = 'https://api.openweathermap.org/data/2.5/'; 2 | 3 | const OPEN_WEATHER_API_APP_ID = ''; 4 | const GOOGLE_PLACES_API_KEY = ''; 5 | 6 | export default { BASE_URL, OPEN_WEATHER_API_APP_ID, GOOGLE_PLACES_API_KEY }; 7 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | import constants from './constants'; 2 | 3 | export { constants }; 4 | -------------------------------------------------------------------------------- /src/config/reactotron.ts: -------------------------------------------------------------------------------- 1 | import Reactotron from 'reactotron-react-native'; 2 | import { reactotronRedux } from 'reactotron-redux'; 3 | import sagaPlugin from 'reactotron-redux-saga'; 4 | 5 | if (__DEV__) { 6 | const tron = Reactotron.configure() 7 | .useReactNative() 8 | .use(reactotronRedux()) 9 | .use(sagaPlugin({})) 10 | .connect(); 11 | 12 | console.tron = tron; 13 | } 14 | -------------------------------------------------------------------------------- /src/navigation/NavigationService.ts: -------------------------------------------------------------------------------- 1 | import { createNavigationContainerRef } from '@react-navigation/native'; 2 | 3 | export const navigationRef = createNavigationContainerRef(); 4 | 5 | export function navigate(name: never, params: never) { 6 | if (navigationRef.isReady()) { 7 | navigationRef.navigate(name, params); 8 | } 9 | } 10 | 11 | export function goBack() { 12 | if (navigationRef.isReady() && navigationRef.canGoBack()) { 13 | navigationRef.goBack(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/navigation/routes.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { NavigationContainer } from '@react-navigation/native'; 4 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 5 | 6 | import { navigationRef } from './NavigationService'; 7 | 8 | import Home from '../screens/Home'; 9 | import Places from '../screens/Places'; 10 | 11 | const Stack = createNativeStackNavigator(); 12 | 13 | export default function Routes() { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/screens/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from 'react'; 2 | import { SafeAreaView } from 'react-native'; 3 | // eslint-disable-next-line import/no-extraneous-dependencies 4 | import { Video } from 'expo-av'; 5 | 6 | import Swiper from 'react-native-swiper'; 7 | 8 | import { useDispatch, useSelector } from 'react-redux'; 9 | 10 | import { RootState } from '~/@types/store/app.state'; 11 | import { InitialNewLocationStateProps } from '../../@types/store/app.state'; 12 | 13 | import WeatherCondition from '~/components/WeatherCondition'; 14 | import Footer from '~/components/Footer'; 15 | 16 | import EmptyAnimation from '~/assets/animations/empty.json'; 17 | import ChurchVideo from '~/assets/videos/church.mp4'; 18 | 19 | import * as NavigationService from '~/navigation/NavigationService'; 20 | 21 | import { 22 | BackgroundVideo, Body, EmptyBody, Animation, EmptyTitle, Container, Button, ButtonTitle 23 | } from './styles'; 24 | import Header from '~/components/Header'; 25 | import { chooseLocationActions } from '../../store/ducks/Location/ChooseLocation'; 26 | import { Ionicons } from '@expo/vector-icons'; 27 | 28 | export default function Home() { 29 | const video = useRef