diff --git a/App/App.js b/App/App.js index fb4e5087..b4afa933 100644 --- a/App/App.js +++ b/App/App.js @@ -1,13 +1,18 @@ -// Copyright (c) 2018, Amaury Martiny and the Shoot! I Smoke contributors +// Copyright (c) 2018, Amaury Martiny // SPDX-License-Identifier: GPL-3.0 -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; import { Font } from 'expo'; +import { Provider } from 'mobx-react'; -import { Screens } from './Screens'; +import { RootStore } from './stores'; import { Background as LoadingBackground } from './Screens/Loading/Background'; +import { Screens } from './Screens'; + +// Set up global MST stores +const stores = RootStore.create({ api: undefined, error: false, location: {} }); -export class App extends Component { +export class App extends PureComponent { state = { fontLoaded: false }; @@ -26,6 +31,12 @@ export class App extends Component { render () { const { fontLoaded } = this.state; - return fontLoaded ? : ; + return fontLoaded ? ( + + + + ) : ( + + ); } } diff --git a/App/App.test.js b/App/App.test.js index 320fbe3b..501a7259 100644 --- a/App/App.test.js +++ b/App/App.test.js @@ -1,4 +1,4 @@ -// Copyright (c) 2018, Amaury Martiny and the Shoot! I Smoke contributors +// Copyright (c) 2018, Amaury Martiny // SPDX-License-Identifier: GPL-3.0 /* eslint-env jest */ diff --git a/App/Screens/About/About.js b/App/Screens/About/About.js new file mode 100644 index 00000000..06286d84 --- /dev/null +++ b/App/Screens/About/About.js @@ -0,0 +1,161 @@ +// Copyright (c) 2018, Amaury Martiny +// SPDX-License-Identifier: GPL-3.0 + +import React, { PureComponent } from 'react'; +import { Constants } from 'expo'; +import { Linking, ScrollView, StyleSheet, Text, View } from 'react-native'; + +import { Box } from './Box'; +import * as theme from '../../utils/theme'; +import { BackButton } from '../../components/BackButton'; + +export class About extends PureComponent { + handleOpenAmaury = () => Linking.openURL('https://twitter.com/amaurymartiny'); + + handleOpenAqi = () => Linking.openURL('http://aqicn.org/'); + + handleOpenArticle = () => + Linking.openURL( + 'http://berkeleyearth.org/air-pollution-and-cigarette-equivalence/' + ); + + handleOpenGithub = () => Linking.openURL(Constants.manifest.extra.githubUrl); + + handleOpenMarcelo = () => + Linking.openURL('https://www.behance.net/marceloscoelho'); + + render () { + const { navigation } = this.props; + return ( + + + + + + How do you calculate the number of cigarettes? + + + This app was inspired by Berkeley Earth’s findings about the{' '} + + equivalence between air pollution and cigarette smoking + + . The rule of thumb is simple: one cigarette per day is the rough + equivalent of a PM2.5 level of 22{' '} + µ + g/m³ + {' \u207D'} + ¹ + {'\u207E'}. + + + + (1){' '} + + http://berkeleyearth.org/air-pollution-and-cigarette-equivalence/ + + + + + + Where does the data come from? + + Air quality data comes from{' '} + + WAQI + {' '} + in the form of PM2.5 AQI levels which are usually updated every one + hour and converted to direct PM2.5 levels by the app. + + + + + + Why is the station so far away from my current location? + + + Since stations that measure and communicate Air Quality results + every hour are expensive, the data is still limited to + well-developed regions and larger cities around the globe. If you + are far from a more prominent urban center, results will probably + not be so accurate. Chances are that your air is better in that case + at least! + + + + + + The results are weird or inconsistent with other sources! + + + We have also encountered a few surprising results: large cities with + better air than small villages; sudden huge increases in the number + of cigarettes; stations of the same town showing significantly + different numbers... The fact is air quality depends on several + factors such as temperature, pressure, humidity and even wind + direction and intensity. If the result seems weird for you, check{' '} + + WAQI + {' '} + for more information and history on your station. + + + + + Credits + + Concept & Development by{' '} + + Amaury Martiny + + .{'\n'} + Design & Copywriting by{' '} + + Marcelo S. Coelho + + .{'\n'} + {'\n'} + Air quality data from{' '} + + WAQI + + .{'\n'} + Source code{' '} + + available on Github + + .{'\n'} + {'\n'} + Shoot! I Smoke v{Constants.manifest.version}. + + + + ); + } +} + +const styles = StyleSheet.create({ + articleLink: { + ...theme.text, + fontSize: 8 + }, + backButton: { + marginBottom: theme.spacing.normal, + marginTop: theme.spacing.normal + }, + credits: { + borderTopColor: theme.iconBackgroundColor, + borderTopWidth: 1, + marginBottom: theme.spacing.normal, + paddingTop: theme.spacing.big + }, + h2: { + ...theme.title, + fontSize: 20, + letterSpacing: 0, + lineHeight: 24, + marginBottom: theme.spacing.small + }, + section: { + marginBottom: theme.spacing.big + } +}); diff --git a/App/Screens/About/Box/Box.js b/App/Screens/About/Box/Box.js new file mode 100644 index 00000000..c038d9f2 --- /dev/null +++ b/App/Screens/About/Box/Box.js @@ -0,0 +1,107 @@ +// Copyright (c) 2018, Amaury Martiny +// SPDX-License-Identifier: GPL-3.0 + +import React, { PureComponent } from 'react'; +import { Image, Platform, StyleSheet, Text, View } from 'react-native'; + +import cigarette from '../../../../assets/images/cigarette.png'; +import * as theme from '../../../utils/theme'; + +export class Box extends PureComponent { + render () { + return ( + + + + + + per day + + = + + 22 + + µ + g/m³ PM2.5* + + + + + *Atmospheric particulate matter (PM) that have a diameter of less than + 2.5 micrometers, with increased chances of inhalation by living + beings. + + + ); + } +} + +const styles = StyleSheet.create({ + box: { + alignItems: 'center', + borderColor: '#EAEAEA', + borderRadius: 8, + borderWidth: 1, + backgroundColor: 'white', + marginTop: 20, + marginBottom: 10, + padding: 10 + }, + boxDescription: { + ...theme.text, + fontSize: 9, + lineHeight: 16, + marginTop: 15 + }, + cigarette: { + left: 6, + position: 'absolute', + bottom: 12 + }, + equal: { + ...theme.text, + color: theme.secondaryTextColor, + fontSize: 44, + lineHeight: 44, + marginHorizontal: 18 + }, + equivalence: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center' + }, + label: { + ...theme.title, + color: theme.secondaryTextColor, + fontSize: 12, + fontWeight: '900', + letterSpacing: 0.5 + }, + micro: { + ...Platform.select({ + ios: { + fontFamily: 'Georgia' + }, + android: { + fontFamily: 'normal' + } + }) + }, + statisticsLeft: { + alignItems: 'flex-end', + justifyContent: 'flex-end', + marginTop: 36, + paddingRight: 10, + width: 90 + }, + statisticsRight: { + alignItems: 'center', + width: 90 + }, + value: { + ...theme.text, + color: theme.secondaryTextColor, + fontSize: 44, + lineHeight: 44 + } +}); diff --git a/App/Screens/About/Box/index.js b/App/Screens/About/Box/index.js new file mode 100644 index 00000000..d1bf25f5 --- /dev/null +++ b/App/Screens/About/Box/index.js @@ -0,0 +1,4 @@ +// Copyright (c) 2018, Amaury Martiny +// SPDX-License-Identifier: GPL-3.0 + +export * from './Box'; diff --git a/App/Screens/About/index.js b/App/Screens/About/index.js new file mode 100644 index 00000000..d455931a --- /dev/null +++ b/App/Screens/About/index.js @@ -0,0 +1,4 @@ +// Copyright (c) 2018, Amaury Martiny +// SPDX-License-Identifier: GPL-3.0 + +export * from './About'; diff --git a/App/Screens/MapScreen/MapScreen.js b/App/Screens/Details/Details.js similarity index 75% rename from App/Screens/MapScreen/MapScreen.js rename to App/Screens/Details/Details.js index 042cf19b..1705b790 100644 --- a/App/Screens/MapScreen/MapScreen.js +++ b/App/Screens/Details/Details.js @@ -1,48 +1,31 @@ -// Copyright (c) 2018, Amaury Martiny and the Shoot! I Smoke contributors +// Copyright (c) 2018, Amaury Martiny // SPDX-License-Identifier: GPL-3.0 import React, { Component } from 'react'; +import { inject, observer } from 'mobx-react'; import { MapView } from 'expo'; import { StyleSheet, View } from 'react-native'; import truncate from 'truncate'; +import { Distance } from './Distance'; import { getCorrectLatLng } from '../../utils/getCorrectLatLng'; -import { Header } from '../../components/Header'; +import { Header } from './Header'; import homeIcon from '../../../assets/images/home.png'; -import { SearchHeader } from '../Search/SearchHeader'; import stationIcon from '../../../assets/images/station.png'; import * as theme from '../../utils/theme'; -export class MapScreen extends Component { - static navigationOptions = { - header: props => { - return ( -
- ); - } - }; - +@inject('stores') +@observer +export class Details extends Component { state = { showMap: false }; - showMapTimeout = null; - - componentWillMount () { + componentDidMount () { // Show map after 200ms for smoother screen transition setTimeout(() => this.setState({ showMap: true }), 500); } - componentWillUnmount () { - clearTimeout(this.showMapTimeout); - } - handleMapReady = () => { this.stationMarker && this.stationMarker.showCallout && @@ -55,10 +38,17 @@ export class MapScreen extends Component { render () { const { - screenProps: { api, currentLocation, onChangeLocationClick } + navigation, + stores: { api, location } } = this.props; const { showMap } = this.state; + // TODO + // I have no idea why, but if we don't clone the object, and continue to + // use `location.current` everywhere, we get a `setting key of frozen + // object` error. It's related to the MapView below. + const currentLocation = { ...location.current }; + const station = { description: api.attributions && api.attributions.length @@ -73,13 +63,7 @@ export class MapScreen extends Component { return ( - +
{showMap && ( )} + ); } diff --git a/App/Screens/Details/Distance/Distance.js b/App/Screens/Details/Distance/Distance.js new file mode 100644 index 00000000..b52221c4 --- /dev/null +++ b/App/Screens/Details/Distance/Distance.js @@ -0,0 +1,38 @@ +// Copyright (c) 2018, Amaury Martiny +// SPDX-License-Identifier: GPL-3.0 + +import React, { Component } from 'react'; +import { inject, observer } from 'mobx-react'; +import { StyleSheet, Text } from 'react-native'; + +import { Banner } from '../../../components/Banner'; +import * as theme from '../../../utils/theme'; + +@inject('stores') +@observer +export class Distance extends Component { + render () { + const { + stores: { distanceToStation } + } = this.props; + + return ( + + + AQI STATION: {distanceToStation}KM AWAY + + + ); + } +} + +const styles = StyleSheet.create({ + banner: { + flexDirection: 'row', + justifyContent: 'center' + }, + distance: { + ...theme.title, + color: 'white' + } +}); diff --git a/App/Screens/Details/Distance/index.js b/App/Screens/Details/Distance/index.js new file mode 100644 index 00000000..37f05b2b --- /dev/null +++ b/App/Screens/Details/Distance/index.js @@ -0,0 +1,4 @@ +// Copyright (c) 2018, Amaury Martiny +// SPDX-License-Identifier: GPL-3.0 + +export * from './Distance'; diff --git a/App/Screens/Details/Header/Header.js b/App/Screens/Details/Header/Header.js new file mode 100644 index 00000000..5c95ad9c --- /dev/null +++ b/App/Screens/Details/Header/Header.js @@ -0,0 +1,112 @@ +// Copyright (c) 2018, Amaury Martiny +// SPDX-License-Identifier: GPL-3.0 + +import React, { Component } from 'react'; +import { formatRelative } from 'date-fns'; +import { Image, StyleSheet, Text, View } from 'react-native'; +import { inject, observer } from 'mobx-react'; + +import { BackButton } from '../../../components/BackButton'; +import locationIcon from '../../../../assets/images/location.png'; +import { CurrentLocation } from '../../../components/CurrentLocation'; +import * as theme from '../../../utils/theme'; + +const trackedPollutant = ['pm25', 'pm10', 'co', 'o3', 'no2', 'so2']; + +@inject('stores') +@observer +export class Header extends Component { + render () { + const { + onBackClick, + stores: { api } + } = this.props; + + const lastUpdated = + api.time && api.time.v ? new Date(api.time.v * 1000) : null; + const { dominentpol, iaqi } = api; + + return ( + + + + + + + + + {lastUpdated && + this.renderInfo( + 'Latest Update:', + formatRelative(lastUpdated, new Date()) + )} + {dominentpol && + this.renderInfo('Primary pollutant:', dominentpol.toUpperCase())} + + + {trackedPollutant.map( + pollutant => + iaqi.get(pollutant) && + this.renderInfo( + `${pollutant.toUpperCase()} AQI:`, + iaqi.get(pollutant).v, + styles.pollutantItem + ) + )} + + + + + ); + } + + renderInfo = (label, value, style = null) => { + return ( + + {label} {value} + + ); + }; +} + +const styles = StyleSheet.create({ + backButton: { + marginBottom: theme.spacing.normal + }, + changeLocation: { + marginRight: theme.spacing.normal + }, + container: { + ...theme.elevatedLevel1('bottom'), + ...theme.withPadding, + backgroundColor: 'white', + paddingBottom: 15, + paddingTop: theme.spacing.normal, + zIndex: 1 + }, + content: { + flex: 1 + }, + currentLocation: { + marginBottom: theme.spacing.normal + }, + info: { + ...theme.text, + marginVertical: 5 + }, + label: { + color: theme.primaryColor, + fontFamily: theme.boldFont + }, + layout: { + flexDirection: 'row' + }, + pollutantItem: { + flexBasis: '34%' + }, + pollutants: { + flexDirection: 'row', + flexWrap: 'wrap', + marginTop: theme.spacing.normal + } +}); diff --git a/App/Screens/Details/Header/index.js b/App/Screens/Details/Header/index.js new file mode 100644 index 00000000..46cf49f7 --- /dev/null +++ b/App/Screens/Details/Header/index.js @@ -0,0 +1,4 @@ +// Copyright (c) 2018, Amaury Martiny +// SPDX-License-Identifier: GPL-3.0 + +export * from './Header'; diff --git a/App/Screens/Details/index.js b/App/Screens/Details/index.js new file mode 100644 index 00000000..482ac076 --- /dev/null +++ b/App/Screens/Details/index.js @@ -0,0 +1,4 @@ +// Copyright (c) 2018, Amaury Martiny +// SPDX-License-Identifier: GPL-3.0 + +export * from './Details'; diff --git a/App/Screens/ErrorScreen/ErrorScreen.js b/App/Screens/ErrorScreen/ErrorScreen.js index 10b76c1a..772d57c8 100644 --- a/App/Screens/ErrorScreen/ErrorScreen.js +++ b/App/Screens/ErrorScreen/ErrorScreen.js @@ -1,16 +1,16 @@ -// Copyright (c) 2018, Amaury Martiny and the Shoot! I Smoke contributors +// Copyright (c) 2018, Amaury Martiny // SPDX-License-Identifier: GPL-3.0 import React, { Component } from 'react'; import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import error from '../../../assets/images/error.png'; -import { Footer } from '../../components/Footer'; import * as theme from '../../utils/theme'; export class ErrorScreen extends Component { + goToSearch = () => this.props.navigation.navigate('Search'); + render () { - const { onChangeLocationClick } = this.props; return ( @@ -25,18 +25,18 @@ export class ErrorScreen extends Component { load your cigarettes. - + + + + CHOOSE OTHER LOCATION - - There's either a problem with our databases, or you don't have any Air Monitoring Stations near you. Try again later! -