├── 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 | 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 |
searchCityName(e)}> 31 | 40 | 41 |
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 | Fork me on GitHub 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 | [![AQI](site/flow.gif)](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 | [![Netlify Status](https://api.netlify.com/api/v1/badges/6cd2087c-70df-4618-8fff-7a4239351f68/deploy-status)](https://app.netlify.com/sites/air-quality-index/deploys) 53 | 54 |
55 | 56 | Liked what I do? Thank You Very Much! 57 | 58 | Buy Me A Coffee 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; --------------------------------------------------------------------------------