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