├── .babelrc
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── images
│ ├── favicon.png
│ ├── weather-icon.png
│ └── weather.png
└── index.html
├── server.js
├── src
├── App.js
├── components
│ ├── Header.js
│ ├── SearchForm.js
│ ├── WeatherApp.js
│ └── WeatherInfo.js
├── logo.svg
├── serviceWorker.js
└── styles
│ ├── base
│ ├── _base.scss
│ └── _settings.scss
│ ├── components
│ ├── _header.scss
│ ├── _searchForm.scss
│ ├── _weatherApp.scss
│ └── _weatherInfo.scss
│ └── styles.scss
├── weatherapp.gif
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/env", "@babel/react"],
3 | "plugins": ["@babel/plugin-proposal-class-properties"]
4 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .env
26 | /config
27 |
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### Weather Search App using React
2 | A simple weather search app in React using OpenWeatherMap's API data.
3 |
4 |
5 |
6 |
7 | ### Autocomplete places feature added
8 | [Autocomplete changes branch in autocomplete-places](https://github.com/skathuria29/react-weather-app/tree/autocomplete-places)
9 |
10 | Using the Places Autocomplete API - [link](https://developers.google.com/maps/documentation/javascript/examples/places-autocomplete)
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "weather",
3 | "version": "0.1.0",
4 | "private": true,
5 | "engines": {
6 | "node": "8.11.1",
7 | "npm": "5.6.0"
8 | },
9 | "dependencies": {
10 | "@babel/cli": "^7.2.3",
11 | "@babel/core": "^7.2.2",
12 | "@babel/plugin-proposal-class-properties": "^7.3.0",
13 | "@babel/polyfill": "^7.2.5",
14 | "@babel/preset-env": "^7.3.1",
15 | "@babel/preset-react": "^7.0.0",
16 | "babel-loader": "^8.0.5",
17 | "css-loader": "^2.1.0",
18 | "dotenv": "^6.2.0",
19 | "moment": "^2.24.0",
20 | "node-sass": "^4.11.0",
21 | "react": "^16.8.0",
22 | "react-dom": "^16.8.0",
23 | "react-scripts": "2.1.3",
24 | "sass-loader": "^7.1.0",
25 | "semantic-ui-react": "^0.85.0",
26 | "style-loader": "^0.23.1"
27 | },
28 | "scripts": {
29 | "build": "webpack",
30 | "dev": "webpack-dev-server",
31 | "postinstall": "webpack -p",
32 | "start": "node server.js"
33 | },
34 | "eslintConfig": {
35 | "extends": "react-app"
36 | },
37 | "browserslist": [
38 | ">0.2%",
39 | "not dead",
40 | "not ie <= 11",
41 | "not op_mini all"
42 | ],
43 | "devDependencies": {
44 | "webpack": "^4.29.2",
45 | "webpack-cli": "^3.2.3",
46 | "webpack-dev-server": "^3.1.14"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/public/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skathuria29/react-weather-app/04c4a2946d6cd2ad6a5196953914922e5015464d/public/images/favicon.png
--------------------------------------------------------------------------------
/public/images/weather-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skathuria29/react-weather-app/04c4a2946d6cd2ad6a5196953914922e5015464d/public/images/weather-icon.png
--------------------------------------------------------------------------------
/public/images/weather.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skathuria29/react-weather-app/04c4a2946d6cd2ad6a5196953914922e5015464d/public/images/weather.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Weather App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const path = require('path');
3 | const PORT = process.env.PORT || 8080;
4 | const app = express();
5 |
6 | // the __dirname is the current directory from where the script is running
7 | app.use(express.static('public'));
8 |
9 | // send the user to index html page inspite of the url
10 |
11 |
12 | app.listen(PORT, function(){
13 | console.log(`App is running on port ${PORT}`);
14 | });
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import '@babel/polyfill';
4 |
5 | import WeatherApp from './components/WeatherApp';
6 | // import 'normalize.css/normalize.css'; //all browsers work on same styles base
7 | import './styles/styles.scss';
8 |
9 | ReactDOM.render(, document.getElementById('app'));
10 |
11 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Header = () => {
4 | return (
5 |
10 | )
11 | }
12 |
13 | export default Header;
--------------------------------------------------------------------------------
/src/components/SearchForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Dropdown } from 'semantic-ui-react';
3 |
4 | const RAPID_API_KEY = `${process.env.RAPID_API_KEY}`;
5 |
6 | class SearchForm extends React.Component {
7 |
8 | state = {
9 | countries: [],
10 | cities: []
11 | }
12 |
13 | getCountryOptions = async () => {
14 |
15 | const getCountriesURL = "https://wft-geo-db.p.rapidapi.com/v1/geo/countries?limit=250";
16 | const apiResponse = await fetch(getCountriesURL, {
17 | headers: {
18 | "X-RapidAPI-Key": RAPID_API_KEY
19 | }
20 | });
21 |
22 | if (apiResponse.status === 200) {
23 | const apiData = await apiResponse.json();
24 | const countriesOptionsData = apiData.data.map((item) => ({ key: item.code, value: item.wikiDataId, text: item.name }));
25 | this.setState(() => ({
26 | countries: countriesOptionsData
27 | }))
28 | }
29 | else {
30 | this.setState(() => ({
31 | error : 'Data Not Found'
32 | }))
33 | }
34 | }
35 |
36 | getCityOptions = async (code) => {
37 |
38 | const getCountryCitiesURL = `https://wft-geo-db.p.rapidapi.com/v1/geo/countries/${code}/regions?limit=100`
39 | const apiResponse = await fetch(getCountryCitiesURL, {
40 | headers: {
41 | "X-RapidAPI-Key": RAPID_API_KEY
42 | }
43 | });
44 | if (apiResponse.status === 200) {
45 | const apiData = await apiResponse.json();
46 |
47 | const citiesOptionsData = apiData.data.map((item) => ({ key: item.isoCode, value: item.name, text: item.name }));
48 | this.setState(() => ({
49 | cities: citiesOptionsData
50 | }))
51 | }
52 | else {
53 | this.setState(() => ({
54 | error: 'Data Not Found'
55 | }))
56 | }
57 | }
58 |
59 | componentDidMount() {
60 | this.getCountryOptions();
61 | }
62 |
63 | handleCountryChange = (e, data) => {
64 | if (data.value) {
65 | const countryCode = data.value.trim();
66 | // this.props.country = data.value;
67 | this.getCityOptions(countryCode);
68 |
69 | }
70 | }
71 |
72 | render() {
73 |
74 | return (
75 |
84 | )
85 | }
86 | }
87 |
88 | export default SearchForm;
--------------------------------------------------------------------------------
/src/components/WeatherApp.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Header from './Header';
3 | import SearchForm from './SearchForm';
4 | import WeatherInfo from './WeatherInfo';
5 | import Config from '../../config/settings';
6 |
7 | // const API_KEY = `${process.env.API_KEY}` || Config['API_KEY'];
8 | const API_KEY = Config['API_KEY'];
9 |
10 | class WeatherApp extends React.Component{
11 |
12 | state = {
13 | city: undefined,
14 | country:undefined,
15 | temperature: undefined,
16 | error : undefined,
17 | today : undefined,
18 | description:undefined,
19 | icon:undefined,
20 | humidity:undefined,
21 | minTemp:undefined,
22 | maxTemp:undefined
23 | }
24 |
25 | getTodaysDate(){
26 | return new Date();
27 | }
28 |
29 |
30 | getWeather = async(e) => {
31 |
32 | e.preventDefault();
33 |
34 | const country = e.target.elements[0].nextElementSibling.innerHTML.trim();
35 | const city = e.target.elements[1].nextElementSibling.innerHTML.trim();
36 |
37 | if(city && country){
38 |
39 | const url = `https://api.openweathermap.org/data/2.5/weather?q=${city},${country}&APPID=${API_KEY}&units=metric`;
40 | const apiResponse = await fetch(url);
41 | const data = await apiResponse.json();
42 |
43 | if(data.cod === "404"){
44 | //data not found
45 | this.setState(()=> ({
46 | error : 'Data Not Found!'
47 | }))
48 | }
49 | else{
50 | this.setState(() => ({
51 | city : data.name,
52 | country : data.sys.country,
53 | temperature: data.main.temp,
54 | today : this.getTodaysDate(),
55 | humidity: data.main.humidity,
56 | minTemp : data.main.temp_min,
57 | maxTemp : data.main.temp_max,
58 | description: data.weather[0].description,
59 | icon: data.weather[0].icon,
60 | error : undefined
61 | }))
62 | }
63 |
64 | }
65 | else{
66 |
67 | this.setState(()=> ({
68 | error : 'Enter Input Values'
69 | }))
70 | }
71 |
72 |
73 | }
74 |
75 | getGradient(temp){
76 |
77 | if(temp < 0)
78 | return 'linear-gradient(to right, #3E5151 ,#DECBA4)';
79 | else if (temp > 0 && temp < 20)
80 | return 'linear-gradient(to right, #bdc3c7, #2c3e50)';
81 | else if (temp > 20 && temp <30)
82 | return 'linear-gradient(to right, #6E86A1, #203543)';
83 | else if(temp > 30)
84 | return 'linear-gradient(to right, rgb(201, 109, 52), rgb(195, 56, 75))';
85 | else
86 | return 'linear-gradient(to right, #5f2c82, #49a09d)';
87 |
88 | }
89 |
90 | render(){
91 | return (
92 |
93 |
94 |
95 |
107 |
108 |
109 | )
110 | }
111 | }
112 |
113 | export default WeatherApp;
--------------------------------------------------------------------------------
/src/components/WeatherInfo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import moment from 'moment';
3 |
4 | const WeatherInfo = (props) => {
5 |
6 | return (
7 |
8 | {props.error &&
{props.error}
}
9 | {!props.error && props.temperature &&
10 |
11 |
12 | {props.temperature} °
13 |
14 |
15 |
16 |
17 |
Humidity
18 |
{props.humidity}
19 |
20 |
21 |
Min
22 |
{props.minTemp} °
23 |
24 |
25 |
Max
26 |
{props.maxTemp} °
27 |
28 |
29 |
Description
30 |
31 | {props.description}
32 |
33 |
34 |
35 |

36 |
37 |
38 |
39 |
40 | }
41 |
42 | )
43 | }
44 |
45 | export default WeatherInfo;
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read http://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/styles/base/_base.scss:
--------------------------------------------------------------------------------
1 | html {
2 | font-size: 62.5%;
3 | height: 100%;
4 | }
5 |
6 | body {
7 | font-family: Helvetica, Arial, sans-serif;
8 | font-size: $m-size;
9 | background: white;
10 | height: 100%;
11 | }
12 |
13 | #app{
14 | height: 100%;
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/src/styles/base/_settings.scss:
--------------------------------------------------------------------------------
1 | // to define theme of the app
2 | //things you want to keep similar/change at multiple places
3 |
4 | //Colors
5 | $off-white : #ebebeb;
6 | $light-gray: #A2B1C0;
7 |
8 |
9 | //fonts
10 | $header-font : 'Fjalla One', sans-serif;
11 | $number-font : 'Lato', sans-serif;
12 |
13 | //spacing
14 | //medium size
15 | $s-size : 1.2rem;
16 | $m-size : 1.6rem;
17 | $l-size : 3.2rem;
18 | $xl-size : 4.8rem;
19 | $desktop-breakpoint : 45rem;
20 |
21 |
22 | $gradient-default : linear-gradient(to right, #6E86A1, #203543);
23 | $gradient-hot : linear-gradient(to right, #E94057 ,#F27121);
24 | $gradient-cold : linear-gradient(to right, #3E5151 ,#DECBA4);
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/styles/components/_header.scss:
--------------------------------------------------------------------------------
1 |
2 | .header{
3 | width: 100%;
4 | margin-bottom: $l-size;
5 | }
6 |
7 | .header .container {
8 | max-width: 60rem;
9 | margin: 0 auto;
10 | padding: $l-size $m-size;
11 | }
12 |
13 | .header__title{
14 | font-size: $xl-size;
15 | text-align: center;
16 | color : $off-white;
17 | font-family : $header-font;
18 | }
--------------------------------------------------------------------------------
/src/styles/components/_searchForm.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 | .search-form-wrapper {
4 | max-width: 65rem;
5 | margin: 0 auto;
6 | margin-bottom: 5rem;
7 | }
8 |
9 | .form-container{
10 | padding: 0 1rem;
11 | }
12 |
13 | .search-form__item{
14 | max-width: 20rem;
15 | }
16 |
17 | .search-form{
18 | display:flex;
19 | justify-content: space-between;
20 | }
21 |
22 | .ui.selection.dropdown .menu>.item{
23 | font-size: 1.4rem;
24 | }
25 |
26 | .search-form__item.btn{
27 | font-size: $m-size;
28 | margin : 0 2rem;
29 | }
30 |
31 |
32 | .ui.search.search-form__item ,
33 | .ui.search.search-form__item:hover,
34 | .ui.search.search-form__item:focus,
35 | .ui.search.search-form__item::selection{
36 | background: inherit;
37 | border: none;
38 | border-bottom: 2px solid $light-gray;
39 | border-radius: 0px;
40 | color: $off-white;
41 | box-shadow: none;
42 | }
43 |
44 | .ui.search.search-form__item input{
45 | color: $off-white;
46 | }
47 |
48 | .ui.selection.visible.dropdown>.text:not(.default){
49 | color: $off-white;
50 | }
51 |
52 | input:-webkit-autofill,
53 | input:-webkit-autofill:hover,
54 | input:-webkit-autofill:focus,
55 | input:-webkit-autofill:active {
56 | -webkit-animation: autofill 0s forwards;
57 | animation: autofill 0s forwards;
58 | }
59 |
60 | @keyframes autofill {
61 | 100% {
62 | background: transparent;
63 | color: $off-white;
64 | }
65 | }
66 |
67 | @-webkit-keyframes autofill {
68 | 100% {
69 | background: transparent;
70 | color: $off-white;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/styles/components/_weatherApp.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 | .wrapper{
4 | height: 100%;
5 | background-image: $gradient-default;
6 | }
--------------------------------------------------------------------------------
/src/styles/components/_weatherInfo.scss:
--------------------------------------------------------------------------------
1 | .weather-info-wrapper{
2 | height: 40rem;
3 | max-width: 70rem;
4 | margin: 0 auto;
5 | }
6 |
7 | .weather-info__temperature{
8 | font-size: 15rem;
9 | color: #ebebeb;
10 | font-family: $number-font;
11 | text-align: center;
12 | height: 150px;
13 | padding-top: 80px;
14 | margin-bottom: 10rem;
15 | }
16 |
17 | .weather-info__description{
18 | display: flex;
19 | flex-direction: row;
20 | justify-content: space-between;
21 | color : $off-white;
22 | font-family : $header-font;
23 | letter-spacing: 1px;
24 | }
25 |
26 | .error-message{
27 | text-align: center;
28 | font-size: $l-size;
29 | font-family: $header-font;
30 | color: $off-white;
31 | }
--------------------------------------------------------------------------------
/src/styles/styles.scss:
--------------------------------------------------------------------------------
1 | @import './base/settings';
2 | @import './base/base';
3 | @import './components/weatherApp';
4 | @import './components/header';
5 | @import './components/searchForm';
6 | @import './components/weatherInfo';
7 |
--------------------------------------------------------------------------------
/weatherapp.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skathuria29/react-weather-app/04c4a2946d6cd2ad6a5196953914922e5015464d/weatherapp.gif
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | // enter entry and out files
2 | const path = require('path');
3 | const webpack = require('webpack');
4 | const dotenv = require('dotenv');
5 | const env = dotenv.config().parsed;
6 |
7 |
8 | // reduce it to a nice object, the same as before
9 | const envKeys = Object.keys(env).reduce((prev, next) => {
10 | prev[`process.env.${next}`] = JSON.stringify(env[next]);
11 | return prev;
12 | }, {});
13 |
14 | module.exports = () => {
15 |
16 | return {
17 | entry: './src/app.js',
18 | output: {
19 | path: path.join(__dirname, 'public'), //this is the abs path on machine you want o/p of that webpack
20 | filename: 'bundle.js'
21 | },
22 | performance: {
23 | hints: false
24 | },
25 | node: {
26 | fs: "empty"
27 | },
28 | module: {
29 | rules: [{
30 | loader: 'babel-loader',
31 | test: /\.js$/, // run through all the js files in your application
32 | exclude: /node_modules/
33 | }, {
34 | test: /\.s?css$/,
35 | use: [
36 | 'style-loader',
37 | 'css-loader',
38 | 'sass-loader'
39 | ]
40 |
41 | }]
42 | }, plugins:[
43 | new webpack.DefinePlugin(envKeys)
44 | ],
45 | devtool: 'cheap-module-eval-source-map',
46 | devServer: {
47 | contentBase: path.join(__dirname, 'public')
48 | }
49 | }
50 | }
51 |
52 |
--------------------------------------------------------------------------------