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