├── .eslintrc ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src ├── components │ ├── App.js │ ├── Header.js │ └── Weather │ │ ├── CurrentWeatherDisplay.js │ │ ├── DailyWeatherDisplay.js │ │ ├── DailyWeatherForecastCard.js │ │ ├── HourlyWeatherDisplay.js │ │ ├── HourlyWeatherForecastCard.js │ │ └── WeatherDashboard.js ├── favicon.ico ├── index.html ├── index.js ├── services │ ├── GeolocationService.js │ └── WeatherService.js └── styles │ ├── _base.scss │ ├── _bootstrap.scss │ ├── _settings.scss │ ├── app.scss │ └── components │ ├── Weather │ ├── _current-weather-display.scss │ ├── _daily-weather-display.scss │ └── _hourly-weather-display.scss │ └── _header.scss └── webpack.config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true 8 | }, 9 | "extends": [ "eslint:recommended", "plugin:react/recommended" ], 10 | "parserOptions": { 11 | "ecmaVersion": 6, 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "sourceType": "module" 16 | }, 17 | "plugins": [ 18 | "react" 19 | ], 20 | "rules": { 21 | "no-console": "off", 22 | "indent": [ 23 | "error", 24 | 4 25 | ], 26 | "quotes": [ 27 | "error", 28 | "single" 29 | ], 30 | "semi": [ 31 | "error", 32 | "always" 33 | ] 34 | } 35 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # environment 7 | .env 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | /public 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | /.vscode/**/* 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Weather 2 | 3 | A weather application that displays the current weather, daily forecasts, and hourly forecasts based on your current geolocation. 4 | 5 | Go **[here](http://react-weather-standard.drminnaar.me/)** for live demo. 6 | 7 | The weather application is composed of the following components: 8 | 9 | * Header - A heading that displays application title 10 | 11 | * WeatherDashboard - The primary (root) component that manages state for all underlying components. It is also responsibly for connecting to and retrieving data from a weather and geolocation service. 12 | 13 | * CurrentWeatherDisplay - Displays weather information for the current point in time based on current location. 14 | 15 | * DailyWeatherDisplay - Displays a 7 day weather forecast in the form of a scrollable carousel. 16 | 17 | * DailyWeatherForecastCard - Displays weather summary for a given day 18 | 19 | * HourlyWeatherDisplay - Displays a 24 hour weather forecast in the form of a scrollable carousel. 20 | 21 | * HourlyWeatherForecastCard - Displays weather summary for a given hour 22 | 23 | Component Diagram 24 | ![Component Diagram](https://user-images.githubusercontent.com/33935506/34460164-e4ecc5a4-ee0f-11e7-8813-574737aab026.png) 25 | 26 | The following services are used to obtain weather and location data: 27 | 28 | * WeatherService - A wrapper that is responsible for integrating with the [OpenWeather Api]. It provides an interface that allows one to obtain current weather, daily forecast, and hourly forecast information. 29 | 30 | * GeolocationService - A wrapper that is responsible for integrating with the [Google Geolocation API]. It provide an interface that allows one to obtain the current GPS coordinates. These coordinates are used by the _WeatherService_ to obtain weather information. 31 | 32 | Service Diagram 33 | ![Service Diagram](https://user-images.githubusercontent.com/33935506/34460198-b860e23a-ee10-11e7-9a34-d2158e43fb10.png) 34 | 35 | Features: 36 | 37 | * Display current weather 38 | * Display 7 day weather forecast 39 | * Display 24 hour weather forecast 40 | 41 | This project also demonstrates: 42 | 43 | * a typcial React project layout structure 44 | * babel setup and configuration 45 | * webpack setup and configuration 46 | * dotenv setup included 47 | * Third party React component integration using _'[React Owl Carousel 2]'_ 48 | * eslint setup and configuration 49 | * SCSS setup and configuration 50 | * [OpenWeather API] integration 51 | * [Google Geolocation API] integration 52 | 53 | **Screenshots:** 54 | 55 | ![React Weather](https://user-images.githubusercontent.com/33935506/34460233-590c5d68-ee11-11e7-9b77-f1c3d8d7536b.png) 56 | 57 | --- 58 | 59 | ## Developed With 60 | 61 | * [Visual Studio Code](https://code.visualstudio.com/) - A source code editor developed by Microsoft for Windows, Linux and macOS. It includes support for debugging, embedded Git control, syntax highlighting, intelligent code completion, snippets, and code refactoring 62 | * [Node.js](https://nodejs.org/en/) - Javascript runtime 63 | * [React](https://reactjs.org/) - A javascript library for building user interfaces 64 | * [Babel](https://babeljs.io/) - A transpiler for javascript 65 | * [Webpack](https://webpack.js.org/) - A module bundler 66 | * [SCSS](http://sass-lang.com/) - A css metalanguage 67 | * [Bootstrap 4](https://getbootstrap.com/) - Bootstrap is an open source toolkit for developing with HTML, CSS, and JS 68 | * [Axios](https://github.com/axios/axios) - Promise based HTTP client for the browser and node.js 69 | * [OpenWeather API] - Provides weather information 70 | * [Google Geolocation API] - Provides geolocation information 71 | * [React Owl Carousel 2] - A third party react carousel component 72 | * [Surge] - Static web publishing for Front-End Developers 73 | 74 | --- 75 | 76 | ## Related Projects 77 | 78 | * [react-starter] 79 | 80 | A basic template that consists of the essential elements that are required to start building a React application 81 | 82 | * [react-clicker] 83 | 84 | A basic React app that allows one to increase, decrease, or reset a counter 85 | 86 | * [react-clock-basic] 87 | 88 | A basic clock that displays the current date and time 89 | 90 | * [react-timer-basic] 91 | 92 | A basic timer that will start a countdown based on an input of time in seconds 93 | 94 | * [react-timer-advanced] 95 | 96 | A countdown timer that offers an advanced UI experience 97 | 98 | * [react-masterminds] 99 | 100 | A basic game of guessing a number with varying degrees of difficulty 101 | 102 | * [react-movie-cards] 103 | 104 | A basic application that displays a list of movies as a list of cards 105 | 106 | * [react-calculator-standard] 107 | 108 | A calculator that provides the essential arithmetic operations, an expression builder, and a complete history of all expressions 109 | 110 | * [react-bitcoin-monitor] 111 | 112 | An app that monitors changes in the Bitcoin Price Index (BPI) 113 | 114 | --- 115 | 116 | ## Getting Started 117 | 118 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. 119 | 120 | ### Prerequisites 121 | 122 | The following software is required to be installed on your system: 123 | 124 | * Node 8.x 125 | * Npm 3.x 126 | 127 | Type the following commands in the terminal to verify your node and npm versions 128 | 129 | ```bash 130 | node -v 131 | npm -v 132 | ``` 133 | 134 | ### Install 135 | 136 | Follow the following steps to get development environment running. 137 | 138 | * Clone _'react-weather-standard'_ repository from GitHub 139 | 140 | ```bash 141 | git clone https://github.com/drminnaar/react-weather-standard.git 142 | ``` 143 | 144 | _OR USING SSH_ 145 | 146 | ```bash 147 | git clone git@github.com:drminnaar/react-weather-standard.git 148 | ``` 149 | 150 | * Install node modules 151 | 152 | ```bash 153 | cd react-weather-standard 154 | npm install 155 | ``` 156 | 157 | Before continuing, the following steps are required: 158 | 159 | 1. Get API keys 160 | 161 | * OpenWeather API 162 | 163 | Have a look at [OpenWeather API](http://openweathermap.org/api) 164 | 165 | Get an API key [here](http://openweathermap.org/appid) 166 | 167 | * Google Geolocation API 168 | 169 | Have a look at [Google Geolocation API] 170 | 171 | Get an API key [here](https://developers.google.com/maps/documentation/geolocation/get-api-key) 172 | 173 | 1. Setup envrionment variables 174 | 175 | One is required to setup a few envrionment variables that are used by the _WeatherService_ and _GeolocationService_ to authenticate against external API's. 176 | 177 | Please follow the following steps: 178 | 179 | * Add _'.env'_ file 180 | 181 | Create a file called _'.env'_ at the root of the application 182 | 183 | * Add environment variables to _'.env'_ file 184 | 185 | GOOGLE_GEOLOCATION_API_KEY=YOUR_API_KEY_GOES_HERE 186 | OPEN_WEATHER_API_KEY=YOUR_API_KEY_GOES_HERE 187 | 188 | For more information about _'.env'_, please visit [dotenv-webpack](https://www.npmjs.com/package/dotenv-webpack) 189 | 190 | ### Build 191 | 192 | * Build application 193 | 194 | This command will also run ESLint as part of build process. 195 | 196 | ```bash 197 | npm run build 198 | ``` 199 | 200 | * Build application and start watching for changes 201 | 202 | This command will also run ESLint as part of build process. 203 | 204 | ```bash 205 | npm run build:watch 206 | ``` 207 | 208 | ### Run ESlint 209 | 210 | * Lint project using ESLint 211 | 212 | ```bash 213 | npm run lint 214 | ``` 215 | 216 | * Lint project using ESLint, and autofix 217 | 218 | ```bash 219 | npm run lint:fix 220 | ``` 221 | 222 | ### Run 223 | 224 | * Run start 225 | 226 | This will run the _'serve'_ npm task 227 | 228 | ```bash 229 | npm start 230 | ``` 231 | 232 | * Run webpack dev server 233 | 234 | ```bash 235 | npm run serve:dev 236 | ``` 237 | 238 | * Alternatively run live-server (simple development http server with live reload capability) 239 | 240 | ```bash 241 | npm run serve 242 | ``` 243 | 244 | --- 245 | 246 | ## Versioning 247 | 248 | I use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/drminnaar/react-weather-standard/tags). 249 | 250 | ## Authors 251 | 252 | * **Douglas Minnaar** - *Initial work* - [drminnaar](https://github.com/drminnaar) 253 | 254 | [OpenWeather API]: http://openweathermap.org 255 | [Google Geolocation API]: https://developers.google.com/maps/documentation/geolocation/intro 256 | [Surge]: https://surge.sh/ 257 | [React Owl Carousel 2]: https://github.com/florinn/react-owl-carousel2 258 | [react-starter]: https://github.com/drminnaar/react-starter 259 | [react-clicker]: https://github.com/drminnaar/react-clicker 260 | [react-clock-basic]: https://github.com/drminnaar/react-clock-basic 261 | [react-timer-basic]: https://github.com/drminnaar/react-timer-basic 262 | [react-timer-advanced]: https://github.com/drminnaar/react-timer-advanced 263 | [react-masterminds]: https://github.com/drminnaar/react-masterminds 264 | [react-movie-cards]: https://github.com/drminnaar/react-movie-cards 265 | [react-calculator-standard]: https://github.com/drminnaar/react-calculator-standard 266 | [react-bitcoin-monitor]: https://github.com/drminnaar/react-bitcoin-monitor 267 | [react-weather-standard]: https://github.com/drminnaar/react-weather-standard -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-weather-standard", 3 | "version": "1.0.0", 4 | "description": "A weather application that provides the current weather, daily forecast, and hourly forecast.", 5 | "scripts": { 6 | "build": "webpack -d", 7 | "build:watch": "webpack --watch", 8 | "lint": "eslint .; exit 0", 9 | "lint:fix": "eslint . --fix", 10 | "serve": "webpack -p && live-server ./public", 11 | "serve:dev": "webpack-dev-server --open", 12 | "start": "npm run serve", 13 | "test": "echo \"No tests available\" && exit 0" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/drminnaar/react-weather-standard.git" 18 | }, 19 | "license": "MIT", 20 | "babel": { 21 | "presets": [ 22 | "env", 23 | "react" 24 | ] 25 | }, 26 | "devDependencies": { 27 | "babel-cli": "^6.26.0", 28 | "babel-core": "^6.26.0", 29 | "babel-eslint": "^8.2.1", 30 | "babel-loader": "^7.1.2", 31 | "babel-preset-env": "^1.6.1", 32 | "babel-preset-react": "^6.24.1", 33 | "clean-webpack-plugin": "^0.1.18", 34 | "css-loader": "^0.28.9", 35 | "eslint": "^4.17.0", 36 | "eslint-loader": "^1.9.0", 37 | "eslint-plugin-react": "^7.6.1", 38 | "file-loader": "^1.1.5", 39 | "html-webpack-plugin": "^2.30.1", 40 | "live-server": "^1.2.0", 41 | "node-sass": "^4.7.2", 42 | "sass-loader": "^6.0.6", 43 | "style-loader": "^0.20.1", 44 | "url-loader": "^0.6.2", 45 | "webpack": "^3.8.1", 46 | "webpack-dev-server": "^2.11.1" 47 | }, 48 | "dependencies": { 49 | "axios": "^0.17.1", 50 | "dotenv": "^5.0.0", 51 | "dotenv-webpack": "^1.5.4", 52 | "prop-types": "^15.6.0", 53 | "react": "^16.2.0", 54 | "react-dom": "^16.2.0", 55 | "react-owl-carousel2": "^0.2.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Header } from './Header'; 3 | import { WeatherDashboard } from './Weather/WeatherDashboard'; 4 | 5 | const App = () => ( 6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 |
14 | ); 15 | 16 | export default App; -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Header = (props) => ( 5 | 13 | ); 14 | 15 | Header.defaultProps = { 16 | title: 'App' 17 | }; 18 | 19 | Header.propTypes = { 20 | title: PropTypes.string 21 | }; 22 | 23 | export { Header }; -------------------------------------------------------------------------------- /src/components/Weather/CurrentWeatherDisplay.js: -------------------------------------------------------------------------------- 1 | // IMPORT PACKAGE REFERENCES 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | 6 | const getUpdateTime = (date) => { 7 | const hours = date.getHours().toString().padStart(2, '0'); 8 | const minutes = date.getMinutes().toString().padEnd(2, '0'); 9 | return `${hours}:${minutes}`; 10 | }; 11 | 12 | const CurrentWeatherDisplay = (props) => { 13 | 14 | const { weather } = props; 15 | 16 | return ( 17 |
18 |
{weather.location.name}
19 |
{weather.temperature.maximum}° | {weather.temperature.minimum}°
20 |
21 | {parseInt(weather.temperature.current)} ° c 22 |
23 |
24 | 25 | {weather.condition} 26 |
27 |
Updated as of {getUpdateTime(weather.date)}
28 | 29 |
30 | ); 31 | }; 32 | 33 | 34 | CurrentWeatherDisplay.propTypes = { 35 | onRefresh: PropTypes.func.isRequired, 36 | weather: PropTypes.object.isRequired 37 | }; 38 | 39 | 40 | export { CurrentWeatherDisplay }; -------------------------------------------------------------------------------- /src/components/Weather/DailyWeatherDisplay.js: -------------------------------------------------------------------------------- 1 | // IMPORT PACKAGE REFERENCES 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import OwlCarousel from 'react-owl-carousel2'; 5 | 6 | // IMPORT PROJECT REFERENCES 7 | import { DailyWeatherForecastCard } from './DailyWeatherForecastCard'; 8 | 9 | 10 | const options = { 11 | items: 3, 12 | nav: false, 13 | rewind: true, 14 | autoplay: false 15 | }; 16 | 17 | 18 | class DailyWeatherDisplay extends Component { 19 | 20 | constructor(props){ 21 | super(props); 22 | } 23 | 24 | 25 | render() { 26 | return ( 27 |
28 |
Daily
29 |
30 | this.carousel = el} options={options}> 31 | { 32 | !!this.props.dailyForecasts && this.props.dailyForecasts.map((fc, i) => ( 33 | 34 | )) 35 | } 36 | 37 |
38 |
39 | this.carousel.prev()}> 40 |
41 |
42 | this.carousel.next()}> 43 |
44 |
45 | ); 46 | } 47 | } 48 | 49 | 50 | DailyWeatherDisplay.propTypes = { 51 | dailyForecasts: PropTypes.array.isRequired 52 | }; 53 | 54 | 55 | export { DailyWeatherDisplay }; -------------------------------------------------------------------------------- /src/components/Weather/DailyWeatherForecastCard.js: -------------------------------------------------------------------------------- 1 | // IMPORT PACKAGE REFERENCES 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | 6 | const weekday = new Array(7); 7 | weekday[0] = 'Sun'; 8 | weekday[1] = 'Mon'; 9 | weekday[2] = 'Tue'; 10 | weekday[3] = 'Wed'; 11 | weekday[4] = 'Thu'; 12 | weekday[5] = 'Fri'; 13 | weekday[6] = 'Sat'; 14 | 15 | 16 | const getDate = (date) => { 17 | return `${weekday[date.getDay()]} ${date.getDate()}`; 18 | }; 19 | 20 | 21 | const DailyWeatherForecastCard = ({ forecast }) => ( 22 |
23 | {getDate(forecast.date)} 24 | 25 |
26 | {parseInt(forecast.temperature.maximum)}° 27 |  {parseInt(forecast.temperature.minimum)}° 28 |
29 |
30 | {forecast.condition} 31 |
32 |
33 | ); 34 | 35 | 36 | DailyWeatherForecastCard.propTypes = { 37 | forecast: PropTypes.object.isRequired 38 | }; 39 | 40 | 41 | export { DailyWeatherForecastCard }; -------------------------------------------------------------------------------- /src/components/Weather/HourlyWeatherDisplay.js: -------------------------------------------------------------------------------- 1 | // IMPORT PACKAGE REFERENCES 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import OwlCarousel from 'react-owl-carousel2'; 5 | 6 | // IMPORT PROJECT REFERENCES 7 | import { HourlyWeatherForecastCard } from './HourlyWeatherForecastCard'; 8 | 9 | 10 | const options = { 11 | items: 3, 12 | nav: false, 13 | rewind: true, 14 | autoplay: false 15 | }; 16 | 17 | 18 | class HourlyWeatherDisplay extends Component { 19 | render(){ 20 | return ( 21 |
22 |
Hourly
23 |
24 | this.carousel = el} options={options}> 25 | { 26 | !!this.props.hourlyForecasts && this.props.hourlyForecasts.map((fc, i) => ( 27 | 28 | )) 29 | } 30 | 31 |
32 |
33 | this.carousel.prev()}> 34 |
35 |
36 | this.carousel.next()}> 37 |
38 |
39 | ); 40 | } 41 | } 42 | 43 | HourlyWeatherDisplay.propTypes = { 44 | hourlyForecasts: PropTypes.array.isRequired 45 | }; 46 | 47 | export { HourlyWeatherDisplay }; -------------------------------------------------------------------------------- /src/components/Weather/HourlyWeatherForecastCard.js: -------------------------------------------------------------------------------- 1 | // IMPORT PACKAGE REFERENCES 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | 6 | const getTime = (date) => { 7 | return `${date.getHours()}:00`; 8 | }; 9 | 10 | 11 | const HourlyWeatherForecastCard = ({ forecast }) => ( 12 |
13 | {getTime(forecast.date)} 14 | 15 |
16 | {parseInt(forecast.temperature.current)}° 17 |
18 |
19 | {forecast.condition} 20 |
21 |
22 | ); 23 | 24 | 25 | HourlyWeatherForecastCard.propTypes = { 26 | forecast: PropTypes.object.isRequired 27 | }; 28 | 29 | 30 | export { HourlyWeatherForecastCard }; -------------------------------------------------------------------------------- /src/components/Weather/WeatherDashboard.js: -------------------------------------------------------------------------------- 1 | // IMPORT PACKAGE REFERENCES 2 | import React, { Component } from 'react'; 3 | 4 | // IMPORT PROJECT REFERENCES 5 | import { CurrentWeatherDisplay } from './CurrentWeatherDisplay'; 6 | import { DailyWeatherDisplay } from './DailyWeatherDisplay'; 7 | import { HourlyWeatherDisplay } from './HourlyWeatherDisplay'; 8 | 9 | // IMPORT PROJECT SERVICES 10 | import { WeatherService } from '../../services/WeatherService'; 11 | import { GeolocationService } from '../../services/GeolocationService'; 12 | 13 | // INITIALIZE SERVICES 14 | const weatherService = new WeatherService(); 15 | const geolocationService = new GeolocationService(); 16 | 17 | 18 | class WeatherDashboard extends Component { 19 | 20 | constructor(props) { 21 | super(props); 22 | 23 | this.state = { 24 | showCurrentWeather: false, 25 | showDailyWeather: false, 26 | showHourlyWeather: false 27 | }; 28 | 29 | this.handleOnRefresh = this.handleOnRefresh.bind(this); 30 | } 31 | 32 | 33 | componentDidMount() { 34 | geolocationService 35 | .getCurrentPosition() 36 | .then(position => { 37 | this.loadCurrentWeatherByPosition(position); 38 | this.loadDailyWeatherByPosition(position); 39 | this.loadHourlyWeatherByPosition(position); 40 | }) 41 | .catch(error => console.log(error)); 42 | } 43 | 44 | 45 | loadCurrentWeatherByPosition(position) { 46 | 47 | if (!position) { 48 | throw Error('A valid position must be specified'); 49 | } 50 | 51 | weatherService 52 | .getCurrentWeatherByPosition(position) 53 | .then(weather => { 54 | this.setState(() => ({ weather: weather, showCurrentWeather: true })); 55 | }) 56 | .catch(error => console.log(error)); 57 | } 58 | 59 | 60 | loadDailyWeatherByPosition(position) { 61 | 62 | if (!position) { 63 | throw Error('A valid position must be specified'); 64 | } 65 | 66 | weatherService 67 | .getDailyWeatherByPosition(position) 68 | .then(dailyForecasts => { 69 | this.setState(() => ({ dailyForecasts: dailyForecasts, showDailyWeather: true })); 70 | }) 71 | .catch(error => console.log(error)); 72 | } 73 | 74 | 75 | loadHourlyWeatherByPosition(position) { 76 | 77 | if (!position) { 78 | throw Error('A valid position must be specified'); 79 | } 80 | 81 | weatherService 82 | .getHourlyWeatherByPosition(position) 83 | .then(hourlyForecasts => { 84 | this.setState(() => ({ hourlyForecasts: hourlyForecasts, showHourlyWeather: true })); 85 | }) 86 | .catch(error => console.log(error)); 87 | } 88 | 89 | 90 | handleOnRefresh() { 91 | this.setState(() => ({ 92 | showCurrentWeather: false, 93 | showDailyWeather: false, 94 | showHourlyWeather: false 95 | })); 96 | 97 | geolocationService 98 | .getCurrentPosition() 99 | .then(position => { 100 | this.loadCurrentWeatherByPosition(position); 101 | this.loadDailyWeatherByPosition(position); 102 | this.loadHourlyWeatherByPosition(position); 103 | }) 104 | .catch(error => console.log(error)); 105 | } 106 | 107 | 108 | showWeather() { 109 | return this.state.showCurrentWeather 110 | && this.state.showDailyWeather 111 | && this.state.showHourlyWeather; 112 | } 113 | 114 | 115 | showSpinner() { 116 | return !this.state.showCurrentWeather 117 | || !this.state.showDailyWeather 118 | || !this.state.showHourlyWeather; 119 | } 120 | 121 | 122 | render() { 123 | return ( 124 |
125 | { 126 | this.showWeather() && 127 |
128 | 129 | 130 | 131 |
132 | } 133 | { 134 | this.showSpinner() && 135 |
136 | 137 |
138 | } 139 |
140 | ); 141 | } 142 | } 143 | 144 | 145 | export { WeatherDashboard }; -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drminnaar/react-weather-standard/9c12f17e79e5af8209b81bcb48172c435f9a1fa9/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Weather 9 | 10 | 12 | 14 | 15 | 16 | 17 |
18 | 19 | 21 | 23 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/App'; 4 | import './styles/app.scss'; 5 | 6 | ReactDOM.render(, document.getElementById('app')); -------------------------------------------------------------------------------- /src/services/GeolocationService.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const BASE_URL = 'https://www.googleapis.com/geolocation/v1/geolocate'; 4 | const API_KEY = process.env.GOOGLE_GEOLOCATION_API_KEY; 5 | 6 | class GeolocationService { 7 | 8 | getCurrentPosition() { 9 | const url = `${BASE_URL}?key=${API_KEY}`; 10 | 11 | return new Promise((resolve, reject) => { 12 | axios 13 | .post(url, { considerIp: true }) 14 | .then(response => { 15 | if (response && response.status === 200) { 16 | const { lat, lng } = response.data.location; 17 | resolve({ 18 | latitude: lat, 19 | longitude: lng 20 | }); 21 | } else { 22 | reject('Unable to retrieve current location'); 23 | } 24 | }) 25 | .catch(error => { 26 | const { errors } = error.response.data.error; 27 | if (errors && errors.length > 0) { 28 | errors.forEach(e => console.log(`Error: ${e.message}, Reason: ${e.reason}`)); 29 | } 30 | }); 31 | }); 32 | } 33 | } 34 | 35 | export { GeolocationService }; -------------------------------------------------------------------------------- /src/services/WeatherService.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const OPEN_WEATHER_BASE_URL = 'http://api.openweathermap.org/data/2.5'; 4 | const OPEN_WEATHER_API_KEY = process.env.OPEN_WEATHER_API_KEY; 5 | const OPEN_WEATHER_IMG_URL = 'http://openweathermap.org/img/w'; 6 | 7 | const getWeather = (url) => { 8 | return new Promise((resolve, reject) => { 9 | axios 10 | .get(url) 11 | .then(response => { 12 | if (response && response.status === 200) { 13 | const { main, icon } = response.data.weather[0]; 14 | const { temp, temp_min, temp_max } = response.data.main; 15 | const { lon, lat } = response.data.coord; 16 | const { dt, name } = response.data; 17 | resolve({ 18 | condition: main, 19 | date: new Date(dt * 1000), 20 | icon: `${OPEN_WEATHER_IMG_URL}/${icon}.png`, 21 | location: { 22 | name: name, 23 | latitude: lat, 24 | longitude: lon 25 | }, 26 | temperature: { 27 | current: temp, 28 | minimum: temp_min, 29 | maximum: temp_max 30 | } 31 | }); 32 | } else { 33 | reject('Weather data not found'); 34 | } 35 | }) 36 | .catch(error => reject(error.message)); 37 | }); 38 | }; 39 | 40 | 41 | const getDailyWeather = (url) => { 42 | return new Promise((resolve, reject) => { 43 | axios 44 | .get(url) 45 | .then(response => { 46 | if (response && response.status === 200) { 47 | 48 | const location = { 49 | name: response.data.city.name, 50 | latitude: response.data.city.coord.lat, 51 | longitude: response.data.city.coord.lon 52 | }; 53 | 54 | const dailyForecasts = response.data.list.map(fc => { 55 | return { 56 | condition: fc.weather[0].description, 57 | date: new Date(fc.dt * 1000), 58 | icon: `${OPEN_WEATHER_IMG_URL}/${fc.weather[0].icon}.png`, 59 | location: location, 60 | temperature: { 61 | minimum: fc.temp.min, 62 | maximum: fc.temp.max 63 | } 64 | }; 65 | }); 66 | 67 | resolve(dailyForecasts); 68 | } else { 69 | reject('Weather data not found'); 70 | } 71 | }) 72 | .catch(error => reject(error.message)); 73 | }); 74 | }; 75 | 76 | 77 | const getHourlyWeather = (url) => { 78 | return new Promise((resolve, reject) => { 79 | axios 80 | .get(url) 81 | .then(response => { 82 | if (response && response.status === 200) { 83 | 84 | const location = { 85 | name: response.data.city.name, 86 | latitude: response.data.city.coord.lat, 87 | longitude: response.data.city.coord.lon 88 | }; 89 | 90 | const hourlyForecasts = response.data.list.map(fc => { 91 | return { 92 | condition: fc.weather[0].description, 93 | date: new Date(fc.dt * 1000), 94 | icon: `${OPEN_WEATHER_IMG_URL}/${fc.weather[0].icon}.png`, 95 | location: location, 96 | temperature: { 97 | current: fc.main.temp 98 | } 99 | }; 100 | }); 101 | 102 | resolve(hourlyForecasts); 103 | } else { 104 | reject('Weather data not found'); 105 | } 106 | }) 107 | .catch(error => reject(error.message)); 108 | }); 109 | }; 110 | 111 | class WeatherService { 112 | 113 | getCurrentWeatherByPosition({latitude, longitude}) { 114 | if (!latitude) { 115 | throw Error('Latitude is required'); 116 | } 117 | 118 | if (!longitude) { 119 | throw Error('Longitude is required'); 120 | } 121 | 122 | const url = `${OPEN_WEATHER_BASE_URL}/weather?appid=${OPEN_WEATHER_API_KEY}&lat=${latitude}&lon=${longitude}&units=metric`; 123 | 124 | return getWeather(url); 125 | } 126 | 127 | 128 | getDailyWeatherByPosition({latitude, longitude}) { 129 | if (!latitude) { 130 | throw Error('Latitude is required'); 131 | } 132 | 133 | if (!longitude) { 134 | throw Error('Longitude is required'); 135 | } 136 | 137 | const url = `${OPEN_WEATHER_BASE_URL}/forecast/daily?appid=${OPEN_WEATHER_API_KEY}&lat=${latitude}&lon=${longitude}&units=metric&cnt=7`; 138 | 139 | return getDailyWeather(url); 140 | } 141 | 142 | 143 | getHourlyWeatherByPosition({latitude, longitude}) { 144 | if (!latitude) { 145 | throw Error('Latitude is required'); 146 | } 147 | 148 | if (!longitude) { 149 | throw Error('Longitude is required'); 150 | } 151 | 152 | const url = `${OPEN_WEATHER_BASE_URL}/forecast?appid=${OPEN_WEATHER_API_KEY}&lat=${latitude}&lon=${longitude}&units=metric&cnt=12`; 153 | 154 | return getHourlyWeather(url); 155 | } 156 | } 157 | 158 | export { WeatherService }; -------------------------------------------------------------------------------- /src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: $smoke; 3 | font-family: $font-family; 4 | height: 100%; 5 | } -------------------------------------------------------------------------------- /src/styles/_bootstrap.scss: -------------------------------------------------------------------------------- 1 | .btn { 2 | cursor: pointer; 3 | } -------------------------------------------------------------------------------- /src/styles/_settings.scss: -------------------------------------------------------------------------------- 1 | $font-family: 'Open Sans', Helvetica, Arial, sans-serif; 2 | $smoke: whitesmoke; -------------------------------------------------------------------------------- /src/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import '_settings.scss'; 2 | @import '_base.scss'; 3 | @import '_bootstrap.scss'; 4 | @import './components/_header.scss'; 5 | @import './components/Weather/_current-weather-display.scss'; 6 | @import './components/Weather/_daily-weather-display.scss'; 7 | @import './components/Weather/_hourly-weather-display.scss'; -------------------------------------------------------------------------------- /src/styles/components/Weather/_current-weather-display.scss: -------------------------------------------------------------------------------- 1 | .current-weather-display { 2 | background-color: #ECEFF1; 3 | text-align: center; 4 | padding: 10px; 5 | width: 100%; 6 | } 7 | 8 | .current-weather-display .refresh { 9 | cursor: pointer; 10 | position: absolute; 11 | bottom: 20px; 12 | right: 20px; 13 | color: #4CAF50; 14 | } 15 | 16 | .current-weather-display .weather-location { 17 | font-size: 1rem; 18 | vertical-align: middle; 19 | } 20 | 21 | .current-weather-display .weather-min-max-temp { 22 | font-size: 1rem; 23 | vertical-align: middle; 24 | } 25 | 26 | .current-weather-display .weather-current .weather-temp { 27 | font-size: 3rem; 28 | vertical-align: middle; 29 | } 30 | 31 | .current-weather-display .weather-condition .weather-icon { 32 | height: 4rem; 33 | vertical-align: middle; 34 | } 35 | 36 | .current-weather-display .weather-condition .weather-description { 37 | font-size: 1.4rem; 38 | vertical-align: middle; 39 | } 40 | 41 | .current-weather-display .weather-update { 42 | font-size: 0.8rem; 43 | vertical-align: middle; 44 | } -------------------------------------------------------------------------------- /src/styles/components/Weather/_daily-weather-display.scss: -------------------------------------------------------------------------------- 1 | $daily-weather-display-height: 170px; 2 | $daily-weather-display-background-color: #CFD8DC; 3 | 4 | .daily-weather-card { 5 | height: $daily-weather-display-height - 20px; 6 | text-align: center; 7 | } 8 | 9 | .daily-weather-card .icon { 10 | height: 50px; 11 | max-width: 50px; 12 | } 13 | 14 | .daily-weather-display { 15 | background-color: $daily-weather-display-background-color; 16 | height: $daily-weather-display-height; 17 | position: relative; 18 | width: 100%; 19 | } 20 | 21 | .daily-weather-display .carousel { 22 | background-color: $daily-weather-display-background-color; 23 | position: absolute; 24 | width: 100%; 25 | } 26 | 27 | .daily-weather-display .carousel-nav { 28 | cursor: pointer; 29 | line-height: $daily-weather-display-height - 20px; 30 | opacity: 0.5; 31 | position: absolute; 32 | z-index: 10; 33 | } 34 | 35 | .daily-weather-display .carousel-nav > i { 36 | vertical-align: middle; 37 | } -------------------------------------------------------------------------------- /src/styles/components/Weather/_hourly-weather-display.scss: -------------------------------------------------------------------------------- 1 | $hourly-weather-display-height: 170px; 2 | $hourly-weather-display-color: lightSteelBlue; 3 | 4 | .hourly-weather-card { 5 | height: $hourly-weather-display-height - 20px; 6 | text-align: center; 7 | } 8 | 9 | .hourly-weather-card .icon { 10 | height: 50px; 11 | max-width: 50px; 12 | } 13 | 14 | .hourly-weather-display { 15 | background-color: $hourly-weather-display-color; 16 | height: $hourly-weather-display-height; 17 | position: relative; 18 | width: 100%; 19 | } 20 | 21 | .hourly-weather-display .carousel { 22 | position: absolute; 23 | width: 100%; 24 | } 25 | 26 | .hourly-weather-display .carousel-nav { 27 | cursor: pointer; 28 | line-height: $hourly-weather-display-height - 20px; 29 | opacity: 0.5; 30 | position: absolute; 31 | z-index: 1; 32 | } 33 | 34 | .hourly-weather-display .carousel-nav > i { 35 | vertical-align: middle; 36 | } -------------------------------------------------------------------------------- /src/styles/components/_header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | height: 100px; 3 | } 4 | 5 | .brand { 6 | overflow-x: hidden; 7 | min-width: 300px; 8 | text-align: center; 9 | width: 100%; 10 | } 11 | 12 | .brand-icon { 13 | color: $smoke; 14 | vertical-align: middle; 15 | } 16 | 17 | .brand-text { 18 | color: $smoke; 19 | font-size: 2rem; 20 | margin-left: 20px; 21 | vertical-align: middle; 22 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CleanWebPackPlugin = require('clean-webpack-plugin'); 3 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 4 | const Dotenv = require('dotenv-webpack'); 5 | 6 | module.exports = { 7 | entry: './src/index.js', 8 | output: { 9 | filename: 'bundle.js', 10 | path: path.resolve(__dirname, 'public') 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | enforce: 'pre', 16 | test: /\.js$/, 17 | loader: 'eslint-loader', 18 | options: { 19 | failOnWarning: true, 20 | failOnerror: true 21 | }, 22 | exclude: /node_modules/ 23 | }, 24 | { 25 | test: /\.js$/, 26 | loader: 'babel-loader', 27 | exclude: /node_modules/ 28 | }, 29 | { 30 | test: /\.s?css$/, 31 | use: [ 'style-loader', 'css-loader', 'sass-loader' ], 32 | exclude: /node_modules/ 33 | }, 34 | { 35 | test: /\.svg$/, 36 | loader: 'url-loader', 37 | exclude: /node_modules/ 38 | } 39 | ] 40 | }, 41 | plugins: [ 42 | new CleanWebPackPlugin([ 'public' ], { root: path.resolve(__dirname)}), 43 | new HtmlWebPackPlugin({ 44 | template: './src/index.html', 45 | favicon: './src/favicon.ico', 46 | inject: false 47 | }), 48 | new Dotenv({ 49 | path: './.env', 50 | safe: false 51 | }) 52 | ], 53 | devtool: 'cheap-module-eval-source-map', 54 | devServer: { 55 | contentBase: path.resolve(__dirname, 'public'), 56 | compress: true, 57 | port: 9000 58 | } 59 | }; --------------------------------------------------------------------------------