├── site
└── flow.gif
├── public
├── robots.txt
├── favicon.ico
├── apple-icon.png
├── manifest.json
└── index.html
├── src
├── search-icon
├── AQIConst.js
├── index.js
├── App.js
├── NoDataFound.js
├── CityAQIList.js
├── useAQIAPIs.js
├── index.css
├── SearchCities.js
├── CityAQI.js
├── App.css
└── CityAQIDetails.js
├── .github
└── FUNDING.yml
├── .gitignore
├── package.json
├── LICENSE
└── README.md
/site/flow.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atapas/aqi-react/HEAD/site/flow.gif
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/src/search-icon:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atapas/aqi-react/HEAD/src/search-icon
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atapas/aqi-react/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [atapas]
4 |
--------------------------------------------------------------------------------
/public/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atapas/aqi-react/HEAD/public/apple-icon.png
--------------------------------------------------------------------------------
/src/AQIConst.js:
--------------------------------------------------------------------------------
1 |
2 | export const TOKEN = '2d71850fc24edb7443b5922b70f3587eabb14119';
3 | export const SEARCH_CITIES_BASE_URL = 'https://api.waqi.info/search/';
4 | export const FEED_AQI_BASE_URL = 'https://api.waqi.info/feed/@';
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 |
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import SearchCities from './SearchCities';
4 | import './App.css';
5 |
6 | function App() {
7 | return (
8 |
9 |
Know Air Quality Index(AQI)
10 |
11 |
12 | );
13 | }
14 |
15 | export default App;
16 |
--------------------------------------------------------------------------------
/src/NoDataFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const NoDataFound = () => {
4 | return(
5 |
6 | No Data Found!
7 |
8 | How about trying out another City Name?
9 |
10 | )
11 | };
12 |
13 | export default NoDataFound;
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "apple-icon.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "apple-icon.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "code",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "moment": "^2.24.0",
7 | "react": "^16.12.0",
8 | "react-dom": "^16.12.0",
9 | "react-scripts": "3.2.0"
10 | },
11 | "scripts": {
12 | "start": "react-scripts start",
13 | "build": "react-scripts build",
14 | "test": "react-scripts test",
15 | "eject": "react-scripts eject"
16 | },
17 | "eslintConfig": {
18 | "extends": "react-app"
19 | },
20 | "browserslist": {
21 | "production": [
22 | ">0.2%",
23 | "not dead",
24 | "not op_mini all"
25 | ],
26 | "development": [
27 | "last 1 chrome version",
28 | "last 1 firefox version",
29 | "last 1 safari version"
30 | ]
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/CityAQIList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import CityAQI from './CityAQI';
4 | import NoDataFound from './NoDataFound';
5 |
6 | const CityAQIList = props => {
7 | let cityList = [];
8 | if (props.data) {
9 | cityList = props.data;
10 | }
11 |
12 | return (
13 |
14 |
15 | {
16 | cityList.length > 0
17 | ?
18 | cityList.map((cityInfo, i) => (
19 | -
20 |
21 |
22 | ))
23 | :
24 |
25 | }
26 |
27 |
28 | )
29 | };
30 |
31 | export default CityAQIList;
--------------------------------------------------------------------------------
/src/useAQIAPIs.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | const useAQIAPIs = (url) => {
4 | const [data, setData] = useState({});
5 | const [initial, setInitial] = useState(true);
6 | const [loading, setLoading] = useState(false);
7 | const [error, setError] = useState('');
8 |
9 | useEffect(() => {
10 | if (url.trim().length === 0) {
11 | return;
12 | }
13 | async function fetchData() {
14 | try {
15 | setInitial(false);
16 | setLoading(true);
17 | let response = await fetch(url);
18 | const json = await response.json();
19 | setData(json);
20 | setLoading(false);
21 | } catch(error) {
22 | setError(error);
23 | }
24 | }
25 |
26 | fetchData();
27 | }, [url]);
28 |
29 | return [ data, loading, initial, error ];
30 | };
31 |
32 | export { useAQIAPIs };
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Tapas Adhikary
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace;
13 | }
14 |
15 | li {
16 | list-style: none;
17 | }
18 |
19 | input[type=text] {
20 | box-sizing: border-box;
21 | border: 2px solid #ccc;
22 | border-radius: 4px;
23 | font-size: 16px;
24 | background-color: white;
25 | background-image: url('./search-icon');
26 | background-position: 10px 10px;
27 | background-repeat: no-repeat;
28 | padding: 12px 20px 12px 40px;
29 | -webkit-transition: width 0.4s ease-in-out;
30 | transition: width 0.4s ease-in-out;
31 | display: block;
32 | width: calc(100% - 16px);
33 | }
34 |
35 |
36 | input[type=submit] {
37 | background-color: #4CAF50;
38 | color: white;
39 | padding: 12px 20px;
40 | border: none;
41 | border-radius: 4px;
42 | cursor: pointer;
43 | float: right;
44 | display: table-cell;
45 | vertical-align: top;
46 | height: 46px;
47 | -webkit-appearance: none;
48 | }
49 |
50 | input[type=submit]:hover {
51 | background-color: #45a049;
52 | }
53 |
54 | form {
55 | display: table;
56 | margin: 0 auto;
57 | }
58 |
59 | label {
60 | display: table-cell;
61 | margin: 0;
62 | vertical-align: top;
63 | }
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/SearchCities.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 |
3 | import { useAQIAPIs } from './useAQIAPIs';
4 |
5 | import { TOKEN, SEARCH_CITIES_BASE_URL } from './AQIConst';
6 |
7 | import CityAQIList from './CityAQIList';
8 |
9 | const SearchCities = () => {
10 | const [url, setUrl] = useState('');
11 | const [cities , loading, initial, error] = useAQIAPIs(url);
12 | const [searchText, setSearchText] = useState('');
13 | const searchInput = useRef(null);
14 |
15 | useEffect(() => {
16 | searchInput.current.focus();
17 | }, []);
18 |
19 | const searchCityName = (event) => {
20 | event.preventDefault();
21 | setUrl(`${SEARCH_CITIES_BASE_URL}?token=${TOKEN}&keyword=${searchText}`);
22 | }
23 |
24 | const handleSearchTextChange = (event) => {
25 | setSearchText(event.target.value);
26 | }
27 | return(
28 |
29 | { error }
30 |
42 | {
43 | loading ?
44 | (loading...)
45 | :
46 | !initial && ()
47 | }
48 |
49 | )
50 | };
51 |
52 | export default SearchCities;
--------------------------------------------------------------------------------
/src/CityAQI.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import moment from 'moment';
4 |
5 | import CityAQIDetails from './CityAQIDetails';
6 |
7 | const CityAQI = props => {
8 | const [showDetails, setShowDetails] = useState(false);
9 |
10 | const aqi = props.cityInfo.aqi;
11 | const placeName = props.cityInfo.station.name;
12 | const atTime = props.cityInfo.time.stime;
13 | const uid = props.cityInfo.uid;
14 |
15 | const getCategorizedAQI = aqi => {
16 | let className = 'unknown';
17 | let impact = 'Unknown';
18 |
19 | if (aqi >= 0 && aqi <= 50) {
20 | impact = 'Good';
21 | className = 'good';
22 | } else if (aqi >= 51 && aqi <= 100) {
23 | impact = 'Moderate';
24 | className = 'moderate';
25 | } else if (aqi >= 101 && aqi <= 150) {
26 | impact = 'Unhealthy for Sensitive Groups';
27 | className = 'unhealthy-sentitive';
28 | } else if (aqi >= 151 && aqi <= 200) {
29 | impact = 'Unhealthy';
30 | className = 'unhealthy';
31 | } else if (aqi >= 201 && aqi <= 300) {
32 | impact = 'Very Unhealthy';
33 | className = 'very-unhealthy';
34 | } else if (aqi >= 301) {
35 | impact = 'Hazardous';
36 | className = 'hazardous';
37 | }
38 |
39 | let catagorized = {};
40 | catagorized['impact'] = impact;
41 | catagorized['className'] = className;
42 |
43 | return catagorized;
44 | };
45 |
46 | const getAtTimeFormatted = time => {
47 | return moment(time).format('h:mm:ss a');
48 | }
49 | return (
50 | setShowDetails(!showDetails)}>
53 |
At { getAtTimeFormatted(atTime) }: { placeName } - { aqi }
54 |
{ getCategorizedAQI(aqi).impact }
55 | { showDetails &&
}
56 |
57 | )
58 | };
59 |
60 | export default CityAQI;
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | aqi-react: Know Air Quality Index
28 |
29 |
30 |
31 |
35 |
42 |
43 |
44 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 |
2 | .App {
3 | text-align: center;
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | justify-content: center;
8 | font-size: calc(10px + 2vmin);
9 | }
10 |
11 | .cityList {
12 | padding-right: 44px;;
13 | }
14 |
15 | .cityInfo {
16 | margin-bottom: 5px;
17 | padding: 10px;
18 | border: 1px solid;
19 | cursor: pointer;
20 | border-radius: 25px;
21 | }
22 |
23 | .cityInfo.good {
24 | background-color: #52B947;
25 | color: #000000;
26 | border-color: #52B947;
27 | }
28 |
29 | .cityInfo.moderate {
30 | background-color: #F3EC19;
31 | color: #000000;
32 | border-color: #F3EC19;
33 | }
34 |
35 | .cityInfo.unhealthy-sentitive {
36 | background-color: #F57E20;
37 | color: #FFFFFF;
38 | border-color: #F57E20;
39 | }
40 |
41 | .cityInfo.unhealthy {
42 | background-color: #ED1D24;
43 | color: #FFFFFF;
44 | border-color: #ED1D24;
45 | }
46 |
47 | .cityInfo.very-unhealthy {
48 | background-color: #7E2B7D;
49 | color: #FFFFFF;
50 | border-color: #7E2B7D;
51 | }
52 |
53 | .cityInfo.hazardous {
54 | background-color: #480D27;
55 | color: #FFFFFF;
56 | border-color: #480D27;
57 | }
58 |
59 | .cityInfo.unknown {
60 | background-color: #ebebeb;
61 | color: #000000;
62 | border-color: #ebebeb;
63 | }
64 |
65 | .cityInfo .details {
66 | background-color: #ebebeb;
67 | color: #000000;
68 | margin: 5px;
69 | padding: 10px;
70 | border-radius: 10px;
71 | font-size: 17px;
72 | }
73 |
74 | .dot {
75 | height: 12px;
76 | width: 14px;
77 | border-radius: 50%;
78 | display: inline-block;
79 | margin-right: 3px;
80 | margin-top: 2px;
81 | }
82 |
83 | .dot.unknown {
84 | background-color: #bbb;
85 | }
86 |
87 | .dot.good {
88 | background-color: #52B947;
89 | }
90 |
91 | .dot.moderate {
92 | background-color: #F3EC19;
93 | }
94 |
95 | .dot.unhealthy-sentitive {
96 | background-color: #F57E20;
97 | }
98 |
99 | .dot.unhealthy {
100 | background-color: #ED1D24;
101 | }
102 |
103 | .dot.very-unhealthy {
104 | background-color: #7E2B7D;
105 | }
106 |
107 | .dot.hazardous {
108 | background-color: #480D27;
109 | }
110 |
111 | .no-data-found {
112 | background-color: #000;
113 | color: #4caf50;
114 | border-radius: 10px;
115 | padding: 10px;
116 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Project `aqi-react`
2 | aqi-react is a project created to know the Air Quality Index of various parts of the world.
3 |
4 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app) to create the frond-end User Interfaces using ReactJs. At the backend, [Air Quality Programmatic APIs](https://aqicn.org/api/) are used to get the details of the Air Quality.
5 |
6 | # What is Air Quality Index(AQI)
7 | An air quality index (AQI) is used by government agencies to communicate to the public how polluted the air currently is or how polluted it is forecast to become.
8 |
9 | Public health risks increase as the AQI rises. Different countries have their own air quality indices, corresponding to different national air quality standards. More details can be found [here](https://en.wikipedia.org/wiki/Air_quality_index).
10 |
11 | # Why is this project Important?
12 | This project was created as a pet project to explain the concepts of reactjs. It has nothing commercial about it. It is with pure learn and share objecives.
13 |
14 | One can learn following concepts of reactJs:
15 | - React Hook Concepts using, `useState`, `useEffect`, `useRef` etc.
16 | - Uasage of React Forms with Controlled Component
17 | - Show-Hide component in React
18 | - Passing Props
19 |
20 | # Demo
21 | A Demo of the project [is running here](https://air-quality-index.netlify.com/).
22 |
23 | # See it in Action
24 | [](https://tapasadhikary.com)
25 |
26 | # Running the Project
27 |
28 | In the project directory, you can run:
29 |
30 | ## `yarn install`
31 | Install the dependencies.
32 |
33 | ## `yarn start`
34 |
35 | Runs the app in the development mode.
36 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
37 |
38 | The page will reload if you make edits.
39 | You will also see any lint errors in the console.
40 |
41 | ## `yarn build`
42 |
43 | Builds the app for production to the `build` folder.
44 | It correctly bundles React in production mode and optimizes the build for the best performance.
45 |
46 | The build is minified and the filenames include the hashes.
47 | Your app is ready to be deployed!
48 |
49 | To learn React, check out the [React documentation](https://reactjs.org/).
50 |
51 | # Deployment Status
52 | [](https://app.netlify.com/sites/air-quality-index/deploys)
53 |
54 |
55 |
56 | Liked what I do? Thank You Very Much!
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/src/CityAQIDetails.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useAQIAPIs } from './useAQIAPIs';
3 |
4 | import { TOKEN, FEED_AQI_BASE_URL } from './AQIConst';
5 |
6 | const CityAQIDetails = props => {
7 |
8 | const [info, error] = useAQIAPIs(
9 | `${FEED_AQI_BASE_URL}${props.uid}/?token=${TOKEN}`
10 | );
11 |
12 | const names = {
13 | 'pm25': "particulate matter 2.5(pm 2.5)",
14 | 'pm10': "particulate matter 10(pm 10)",
15 | 'o3': "Ozone",
16 | 'no2': "Nitrogen Dioxide",
17 | 'so2': "Sulphur Dioxide",
18 | 'co': "Carbon Monoxyde",
19 | 't': "Temperature",
20 | 'w': "Wind",
21 | 'r': "Rain (precipitation)",
22 | 'h': "Relative Humidity",
23 | 'd': "Dew",
24 | 'p': "Atmostpheric Pressure"
25 | }
26 |
27 | const getSpectrum = iaqi => {
28 | let ret = [];
29 | Object.entries(iaqi).map(function(item) {
30 | let obj = {};
31 | let key = names[item[0]] ? names[item[0]] : item[0];
32 | obj['key'] = key;
33 | obj['value'] = item[1].v;
34 | ret.push(obj);
35 | });
36 | return ret;
37 | }
38 |
39 | const colorize = (name, value) => {
40 | if ([
41 | 'particulate matter 2.5(pm 2.5)',
42 | 'particulate matter 10(pm 10)',
43 | 'Ozone',
44 | 'Nitrogen Dioxide',
45 | 'Sulphur Dioxide',
46 | 'Carbon Monoxyde'].indexOf(name) < 0) {
47 | return '';
48 | }
49 | if (value >= 0 && value <= 50) {
50 | return 'good';
51 | } else if (value >= 51 && value <= 100) {
52 | return 'moderate';
53 | } else if (value >= 101 && value <= 150) {
54 | return 'unhealthy-sentitive';
55 | } else if (value >= 151 && value <= 200) {
56 | return 'unhealthy';
57 | } else if (value >= 201 && value <= 300) {
58 | return 'very-unhealthy';
59 | } else if (value >= 301) {
60 | return 'hazardous';
61 | }
62 | }
63 |
64 |
65 | return(
66 |
67 | {error}
68 | {
69 | info.data ?
70 |
71 |
72 |
73 | Prominent Pollutant is, { names[info.data.dominentpol] }
74 |
75 |
76 |
77 | {
78 | getSpectrum(info.data.iaqi).map((spectrum, i) => (
79 | -
80 |
81 | {spectrum.key}: {spectrum.value}
82 |
83 | ))
84 | }
85 |
86 |
87 | :
88 | Loading...
89 | }
90 |
91 |
92 |
93 | )
94 | };
95 |
96 | export default CityAQIDetails;
--------------------------------------------------------------------------------