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!
-
);
@@ -46,18 +46,17 @@ export class ErrorScreen extends Component {
const styles = StyleSheet.create({
chooseOther: {
...theme.bigButton,
- marginTop: 22
+ marginVertical: theme.spacing.normal
},
container: {
...theme.fullScreen,
...theme.withPadding,
flexGrow: 1,
flexDirection: 'column',
- justifyContent: 'space-between'
+ justifyContent: 'space-around'
},
errorDescription: {
- ...theme.text,
- ...theme.paragraph
+ ...theme.text
},
errorText: {
...theme.shitText,
diff --git a/App/Screens/ErrorScreen/index.js b/App/Screens/ErrorScreen/index.js
index 270241ee..52f5f9e5 100644
--- a/App/Screens/ErrorScreen/index.js
+++ b/App/Screens/ErrorScreen/index.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
export * from './ErrorScreen';
diff --git a/App/Screens/Home/Cigarettes/Cigarette/Cigarette.js b/App/Screens/Home/Cigarettes/Cigarette/Cigarette.js
index 1eac1106..b9afe001 100644
--- a/App/Screens/Home/Cigarettes/Cigarette/Cigarette.js
+++ b/App/Screens/Home/Cigarettes/Cigarette/Cigarette.js
@@ -1,7 +1,7 @@
-// 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 { Image, StyleSheet, View } from 'react-native';
import butt from '../../../../../assets/images/butt.png';
@@ -9,7 +9,7 @@ import buttVertical from '../../../../../assets/images/butt-vertical.png';
import head from '../../../../../assets/images/head.png';
import headVertical from '../../../../../assets/images/head-vertical.png';
-export class Cigarette extends Component {
+export class Cigarette extends PureComponent {
static defaultProps = {
length: 1
};
diff --git a/App/Screens/Home/Cigarettes/Cigarette/index.js b/App/Screens/Home/Cigarettes/Cigarette/index.js
index 3fb08338..c7a6a54b 100644
--- a/App/Screens/Home/Cigarettes/Cigarette/index.js
+++ b/App/Screens/Home/Cigarettes/Cigarette/index.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
export * from './Cigarette';
diff --git a/App/Screens/Home/Cigarettes/Cigarettes.js b/App/Screens/Home/Cigarettes/Cigarettes.js
index ac4e8270..568d6273 100644
--- a/App/Screens/Home/Cigarettes/Cigarettes.js
+++ b/App/Screens/Home/Cigarettes/Cigarettes.js
@@ -1,12 +1,14 @@
-// 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 { StyleSheet, View } from 'react-native';
import { Cigarette } from './Cigarette';
-import { pm25ToCigarettes } from '../../../utils/pm25ToCigarettes';
+@inject('stores')
+@observer
export class Cigarettes extends Component {
getSize = cigarettes => {
if (cigarettes <= 1) return 'big';
@@ -16,9 +18,8 @@ export class Cigarettes extends Component {
};
render () {
- const { pm25, style } = this.props;
- const cigarettes =
- Math.round(Math.min(pm25ToCigarettes(pm25), 63) * 10) / 10; // We don't show more than 63
+ const { stores, style } = this.props;
+ const cigarettes = Math.round(Math.min(stores.cigarettes, 63) * 10) / 10; // We don't show more than 63
// const cigarettes = 0.9; // Can change values here for testing
const count = Math.floor(cigarettes);
diff --git a/App/Screens/Home/Cigarettes/index.js b/App/Screens/Home/Cigarettes/index.js
index bb0de342..92ba6d35 100644
--- a/App/Screens/Home/Cigarettes/index.js
+++ b/App/Screens/Home/Cigarettes/index.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
export * from './Cigarettes';
diff --git a/App/Screens/Home/Header/Header.js b/App/Screens/Home/Header/Header.js
new file mode 100644
index 00000000..300e10ac
--- /dev/null
+++ b/App/Screens/Home/Header/Header.js
@@ -0,0 +1,80 @@
+// Copyright (c) 2018, Amaury Martiny
+// SPDX-License-Identifier: GPL-3.0
+
+import React, { Component } from 'react';
+import { inject, observer } from 'mobx-react';
+import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+import changeLocation from '../../../../assets/images/changeLocation.png';
+import { CurrentLocation } from '../../../components/CurrentLocation';
+import * as theme from '../../../utils/theme';
+import warning from '../../../../assets/images/warning.png';
+
+@inject('stores')
+@observer
+export class Header extends Component {
+ render () {
+ const {
+ onChangeLocationClick,
+ stores: { api, distanceToStation, isStationTooFar, location }
+ } = this.props;
+
+ return (
+
+
+
+
+
+ {isStationTooFar && (
+
+ )}
+
+ Air Quality Station: {distanceToStation}
+ km away
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ backButton: {
+ marginBottom: theme.spacing.normal
+ },
+ changeLocation: {
+ marginRight: theme.spacing.tiny
+ },
+ container: {
+ padding: theme.spacing.normal
+ },
+ content: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between'
+ },
+ currentLocation: {
+ maxWidth: '80%'
+ },
+ distance: {
+ flexDirection: 'row',
+ marginTop: 11
+ },
+ title: {
+ ...theme.title,
+ fontSize: 15
+ },
+ warning: {
+ marginRight: theme.spacing.tiny
+ }
+});
diff --git a/App/Screens/Home/Header/index.js b/App/Screens/Home/Header/index.js
new file mode 100644
index 00000000..46cf49f7
--- /dev/null
+++ b/App/Screens/Home/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/Home/Home.js b/App/Screens/Home/Home.js
index 60a61683..0edb832c 100644
--- a/App/Screens/Home/Home.js
+++ b/App/Screens/Home/Home.js
@@ -1,7 +1,8 @@
-// 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 {
ScrollView,
Share,
@@ -12,60 +13,108 @@ import {
} from 'react-native';
import { Cigarettes } from './Cigarettes';
-import { Footer } from '../../components/Footer';
-import { Header } from '../../components/Header';
-import { pm25ToCigarettes } from '../../utils/pm25ToCigarettes';
+import { Header } from './Header';
+import { SmallButton } from './SmallButton';
+import { SmokeVideo } from './SmokeVideo';
import * as theme from '../../utils/theme';
+@inject('stores')
+@observer
export class Home extends Component {
- static navigationOptions = {
- header: props => {
- return (
- props.navigation.navigate('Map')} // TODO Possible not to create a new function every time?
- showChangeLocation
- />
- );
- }
- };
+ goToAbout = () => this.props.navigation.navigate('About');
- goToMap = () => this.props.navigation.navigate('Map');
+ goToDetails = () => this.props.navigation.navigate('Details');
+
+ goToSearch = () => this.props.navigation.navigate('Search');
handleShare = () =>
Share.share({
title:
'Did you know that you may be smoking up to 20 cigarettes per day, just for living in a big city?',
- message: `Shoot! I 'smoked' ${pm25ToCigarettes(
- this.props.screenProps.api.pm25
- )} cigarettes today by breathing urban air. And you? Find out here: shootismoke.github.io`
+ message: `Shoot! I 'smoked' ${
+ this.props.stores.cigarettes
+ } cigarettes today by breathing urban air. And you? Find out here: https://shootismoke.github.io`
});
render () {
const {
- screenProps: {
- api: { pm25 }
- }
+ stores: { isStationTooFar }
} = this.props;
return (
-
-
-
- {this.renderText()}
-
-
- SHARE WITH YOUR FRIENDS
-
-
+
+
+
+
+
+
+
+ {this.renderText()}
+
+
+ {isStationTooFar && (
+
+ We couldn’t find a closer station to you.{'\n'}Results may be
+ inaccurate at this distance.
+
+ )}
+ {this.renderBigButton()}
+ {this.renderFooter()}
+
+
+
+ );
+ }
+
+ renderBigButton = () => {
+ const {
+ stores: { isStationTooFar }
+ } = this.props;
+ if (isStationTooFar) {
+ return (
+
+
+ WHY IS THE STATION SO FAR?
+
+
+ );
+ }
+
+ return (
+
+
+ SEE DETAILED INFO
+
+ );
+ };
-
-
+ renderFooter = () => {
+ const {
+ stores: { isStationTooFar }
+ } = this.props;
+ return (
+
+ {isStationTooFar ? (
+
+ ) : (
+
+ )}
+
+
);
- }
+ };
renderPresentPast = () => {
const time = new Date().getHours();
@@ -76,11 +125,8 @@ export class Home extends Component {
renderShit = () => {
const {
- screenProps: {
- api: { pm25 }
- }
+ stores: { cigarettes }
} = this.props;
- const cigarettes = pm25ToCigarettes(pm25);
if (cigarettes <= 1) return 'Oh';
if (cigarettes < 5) return 'Sh*t';
@@ -89,16 +135,12 @@ export class Home extends Component {
};
renderText = () => {
- const {
- screenProps: {
- api: { pm25 }
- }
- } = this.props;
+ const { stores } = this.props;
// Round to 1 decimal
- const cigarettes = Math.round(pm25ToCigarettes(pm25) * 10) / 10;
+ const cigarettes = Math.round(stores.cigarettes * 10) / 10;
return (
-
+
{this.renderShit()}! {this.renderPresentPast()}{' '}
{cigarettes} cigarette
@@ -120,24 +162,34 @@ const styles = StyleSheet.create({
},
content: {
...theme.withPadding,
- flexDirection: 'column',
- flexGrow: 1,
justifyContent: 'center'
},
+ cta: {
+ ...theme.withPadding
+ },
dots: {
color: theme.primaryColor
},
- footer: {
- ...theme.withPadding
+ isStationTooFar: {
+ ...theme.text,
+ marginVertical: theme.spacing.normal
},
main: {
- marginBottom: 22
+ marginBottom: theme.spacing.normal
},
- shareButton: {
- alignItems: 'flex-start'
+ scrollContainer: {
+ flexGrow: 1,
+ justifyContent: 'space-around'
},
+ scrollView: { flex: 1 },
shit: {
...theme.shitText,
- marginTop: 22
+ marginTop: theme.spacing.normal
+ },
+ smallButtons: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ marginBottom: 2 * theme.spacing.normal,
+ marginTop: theme.spacing.normal
}
});
diff --git a/App/Screens/Home/SmallButton/SmallButton.js b/App/Screens/Home/SmallButton/SmallButton.js
new file mode 100644
index 00000000..f5902c13
--- /dev/null
+++ b/App/Screens/Home/SmallButton/SmallButton.js
@@ -0,0 +1,36 @@
+// Copyright (c) 2018, Amaury Martiny
+// SPDX-License-Identifier: GPL-3.0
+
+import React from 'react';
+import { FontAwesome } from '@expo/vector-icons';
+import { StyleSheet, Text, TouchableOpacity } from 'react-native';
+
+import * as theme from '../../../utils/theme';
+
+export const SmallButton = ({ text, icon, ...rest }) => (
+
+ {icon && (
+
+ )}
+ {text}
+
+);
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row'
+ },
+ icon: {
+ marginRight: theme.spacing.tiny
+ },
+ title: {
+ ...theme.title,
+ color: theme.primaryColor,
+ lineHeight: 15
+ }
+});
diff --git a/App/Screens/Home/SmallButton/index.js b/App/Screens/Home/SmallButton/index.js
new file mode 100644
index 00000000..3589b426
--- /dev/null
+++ b/App/Screens/Home/SmallButton/index.js
@@ -0,0 +1,4 @@
+// Copyright (c) 2018, Amaury Martiny
+// SPDX-License-Identifier: GPL-3.0
+
+export * from './SmallButton';
diff --git a/App/Screens/Home/SmokeVideo/SmokeVideo.js b/App/Screens/Home/SmokeVideo/SmokeVideo.js
new file mode 100644
index 00000000..413a3e30
--- /dev/null
+++ b/App/Screens/Home/SmokeVideo/SmokeVideo.js
@@ -0,0 +1,47 @@
+// Copyright (c) 2018, Amaury Martiny
+// SPDX-License-Identifier: GPL-3.0
+
+import React, { Component } from 'react';
+import { Dimensions, StyleSheet } from 'react-native';
+import { inject, observer } from 'mobx-react';
+import { Video } from 'expo';
+
+import smokeVideo from '../../../../assets/video/smoke.mp4';
+
+@inject('stores')
+@observer
+export class SmokeVideo extends Component {
+ getVideoStyle = () => {
+ const {
+ stores: { cigarettes }
+ } = this.props;
+
+ if (cigarettes <= 1) return { opacity: 0.2 };
+ if (cigarettes < 5) return { opacity: 0.5 };
+ if (cigarettes < 15) return { opacity: 0.7 };
+ return { opacity: 1 };
+ };
+
+ render () {
+ return (
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ video: {
+ bottom: 0,
+ height: Dimensions.get('screen').height,
+ position: 'absolute',
+ right: 0,
+ width: Dimensions.get('screen').width,
+ zIndex: -1
+ }
+});
diff --git a/App/Screens/Home/SmokeVideo/index.js b/App/Screens/Home/SmokeVideo/index.js
new file mode 100644
index 00000000..f2578774
--- /dev/null
+++ b/App/Screens/Home/SmokeVideo/index.js
@@ -0,0 +1,4 @@
+// Copyright (c) 2018, Amaury Martiny
+// SPDX-License-Identifier: GPL-3.0
+
+export * from './SmokeVideo';
diff --git a/App/Screens/Home/index.js b/App/Screens/Home/index.js
index 368a457b..fa10d1e8 100644
--- a/App/Screens/Home/index.js
+++ b/App/Screens/Home/index.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
export * from './Home';
diff --git a/App/Screens/Loading/Background.js b/App/Screens/Loading/Background.js
index a028c286..77f808ee 100644
--- a/App/Screens/Loading/Background.js
+++ b/App/Screens/Loading/Background.js
@@ -1,12 +1,12 @@
-// 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 { StyleSheet, View } from 'react-native';
import * as theme from '../../utils/theme';
-export class Background extends Component {
+export class Background extends PureComponent {
render () {
return (
diff --git a/App/Screens/Loading/Loading.js b/App/Screens/Loading/Loading.js
index e9d6711e..e88937fd 100644
--- a/App/Screens/Loading/Loading.js
+++ b/App/Screens/Loading/Loading.js
@@ -1,26 +1,32 @@
-// Copyright (c) 2018, Amaury Martiny and the Shoot! I Smoke contributors
+// Copyright (c) 2018, Amaury Martiny
// SPDX-License-Identifier: GPL-3.0
-import PropTypes from 'prop-types';
import React, { Component } from 'react';
+import { inject, observer } from 'mobx-react';
+import { Location, Permissions } from 'expo';
+import retry from 'async-retry';
import { StyleSheet, Text } from 'react-native';
import { Background } from './Background';
+import * as dataSources from '../../utils/dataSources';
import * as theme from '../../utils/theme';
+@inject('stores')
+@observer
export class Loading extends Component {
- static propTypes = {
- gps: PropTypes.object
- };
-
state = {
longWaiting: false // If api is taking a long time
};
longWaitingTimeout = null; // The variable returned by setTimeout for longWaiting
- componentWillReceiveProps ({ gps }) {
- if (!this.props.gps && gps) {
+ componentDidMount () {
+ this.fetchData();
+ }
+
+ componentDidUpdate (prevProps) {
+ if (!prevProps.stores.gps && this.props.gps) {
+ // Start a 2s timeout to occupy user while he's waiting.
this.longWaitingTimeout = setTimeout(
() => this.setState({ longWaiting: true }),
2000 // Set longWaiting after 2s of waiting
@@ -34,6 +40,60 @@ export class Loading extends Component {
}
}
+ async fetchData () {
+ const { stores } = this.props;
+ const { location } = stores;
+
+ try {
+ // The current { latitude, longitude } the user chose
+ let currentPosition = location.current;
+
+ this.setState({ error: null });
+
+ // If the currentLocation has been set by the user, then we don't refetch
+ // the user's GPS
+ if (!currentPosition) {
+ const { status } = await Permissions.askAsync(Permissions.LOCATION);
+ if (status !== 'granted') {
+ throw new Error('Permission to access location was denied.');
+ }
+
+ const { coords } = await Location.getCurrentPositionAsync({});
+ // Uncomment to get other locations
+ // const coords = {
+ // latitude: Math.random() * 90,
+ // longitude: Math.random() * 90
+ // };
+ // const coords = {
+ // latitude: 48.4,
+ // longitude: 2.34
+ // };
+
+ currentPosition = coords;
+
+ location.setCurrent(coords);
+ location.setGps(coords);
+ }
+
+ // We currently have 2 sources, aqicn, and windWaqi
+ // We put them in an array
+ const sources = [dataSources.aqicn, dataSources.windWaqi];
+
+ const _api = await retry(
+ async (_, attempt) => {
+ // Attempt starts at 1
+ const result = await sources[(attempt - 1) % 2](currentPosition);
+ return result;
+ },
+ { retries: 3 } // 2 attemps per source
+ );
+
+ stores.setApi(_api);
+ } catch (error) {
+ stores.setError(true);
+ }
+ }
+
render () {
return (
@@ -50,11 +110,17 @@ export class Loading extends Component {
);
renderText = () => {
- const { gps } = this.props;
+ const {
+ stores: {
+ api,
+ location: { gps }
+ }
+ } = this.props;
const { longWaiting } = this.state;
let coughs = 0; // Number of times to show "Cough..."
if (gps) ++coughs;
if (longWaiting) ++coughs;
+ if (api) ++coughs;
return (
diff --git a/App/Screens/Loading/index.js b/App/Screens/Loading/index.js
index 4362e94f..e9fa2bed 100644
--- a/App/Screens/Loading/index.js
+++ b/App/Screens/Loading/index.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
export * from './Loading';
diff --git a/App/Screens/MapScreen/index.js b/App/Screens/MapScreen/index.js
deleted file mode 100644
index 678ea73b..00000000
--- a/App/Screens/MapScreen/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// Copyright (c) 2018, Amaury Martiny and the Shoot! I Smoke contributors
-// SPDX-License-Identifier: GPL-3.0
-
-export * from './MapScreen';
diff --git a/App/Screens/Screens.js b/App/Screens/Screens.js
index 3dcf493d..89f6581b 100644
--- a/App/Screens/Screens.js
+++ b/App/Screens/Screens.js
@@ -1,197 +1,104 @@
-// 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 { Dimensions, StyleSheet, View } from 'react-native';
-import { Location, Permissions, Video } from 'expo';
-import retry from 'async-retry';
+import { StyleSheet, View } from 'react-native';
+import { inject, observer } from 'mobx-react';
import { createStackNavigator } from 'react-navigation';
-import * as dataSources from '../utils/dataSources';
+import { About } from './About';
+import { Details } from './Details';
import { ErrorScreen } from './ErrorScreen';
import { Home } from './Home';
import { Loading } from './Loading';
-import { MapScreen } from './MapScreen';
-import { pm25ToCigarettes } from '../utils/pm25ToCigarettes';
import { Search } from './Search';
-import smokeVideo from '../../assets/video/smoke.mp4';
import * as theme from '../utils/theme';
+const stackNavigatorOptions = initialRouteName => ({
+ cardStyle: {
+ backgroundColor: theme.backgroundColor
+ },
+ headerMode: 'none',
+ initialRouteName,
+ navigationOptions: {
+ headerVisible: false
+ }
+});
+
+/**
+ * The main stack navigator, for the app.
+ */
const RootStack = createStackNavigator(
{
- Error: {
- screen: ErrorScreen
+ About: {
+ screen: About
+ },
+ Details: {
+ screen: Details
},
Home: {
screen: Home
},
- Map: {
- screen: MapScreen
+ Search: {
+ screen: Search
}
},
+ stackNavigatorOptions('Home')
+);
+
+/**
+ * A second stack navigator, for the error case.
+ */
+const ErrorStack = createStackNavigator(
{
- cardStyle: {
- backgroundColor: 'transparent',
- elevation: 0,
- shadowOpacity: 0
- },
- initialRouteName: 'Home',
- navigationOptions: {
- gesturesEnabled: false,
- headerStyle: {
- elevation: 0,
- shadowOpacity: 0
- },
- headerTransparent: true
+ Error: {
+ screen: ErrorScreen
},
- transitionConfig: () => ({
- containerStyle: {
- backgroundColor: 'transparent'
- }
- })
- }
+ Search: {
+ screen: Search
+ }
+ },
+ stackNavigatorOptions('Error')
);
+@inject('stores')
+@observer
export class Screens extends Component {
state = {
- api: null,
- currentLocation: null, // Initialized to GPS, but can be changed by user
- error: null, // Error here or in children component tree
- gps: null,
- isSearchVisible: false,
- showVideo: true
+ showVideo: true // Showing video or not
};
- componentWillMount () {
- this.fetchData();
- }
-
componentDidCatch (error) {
- this.setState({ error });
- }
-
- async fetchData () {
- const { currentLocation } = this.state;
- try {
- let currentPosition = currentLocation; // The current { latitude, longitude } the user chose
-
- this.setState({ api: null, error: null });
-
- // If the currentLocation has been set by the user, then we don't refetch
- // the user's GPS
- if (!currentPosition) {
- const { status } = await Permissions.askAsync(Permissions.LOCATION);
- if (status !== 'granted') {
- throw new Error('Permission to access location was denied.');
- }
-
- const { coords } = await Location.getCurrentPositionAsync({});
- currentPosition = coords;
-
- // Uncomment to get random location
- // coords = {
- // latitude: Math.random() * 90,
- // longitude: Math.random() * 90
- // };
-
- this.setState({
- currentLocation: coords,
- gps: coords
- });
- }
-
- // We currently have 2 sources, aqicn, and windWaqi
- // We put them in an array
- const sources = [dataSources.aqicn, dataSources.windWaqi];
-
- const api = await retry(
- async (_, attempt) => {
- // Attempt starts at 1
- const result = await sources[(attempt - 1) % 2](currentPosition);
- return result;
- },
- { retries: 3 } // 2 attemps per source
- );
- this.setState({ api });
- } catch (error) {
- console.error(error);
- this.setState({ error });
- }
+ this.props.stores.setError(error);
}
-
- getVideoStyle = () => {
- const {
- api: { pm25 }
- } = this.state;
- const cigarettes = pm25ToCigarettes(pm25);
-
- if (cigarettes <= 1) return { opacity: 0.2 };
- if (cigarettes < 5) return { opacity: 0.5 };
- if (cigarettes < 15) return { opacity: 0.7 };
- return { opacity: 1 };
- };
-
- handleLocationChanged = currentLocation => {
- this.setState({ currentLocation, isSearchVisible: false }, this.fetchData);
- };
-
- handleSearchHide = () => this.setState({ isSearchVisible: false });
-
- handleSearchShow = () => this.setState({ isSearchVisible: true });
-
- handleVideoStatus = ({ didJustFinish }) => {
- if (didJustFinish && this.state.showVideo) {
- this.setState({ showVideo: false });
- }
- };
-
render () {
- const { gps, isSearchVisible } = this.state;
- return (
-
- {this.renderScreen()}
-
-
- );
+ return {this.renderScreen()};
}
renderScreen = () => {
- const { api, currentLocation, error, gps, showVideo } = this.state;
+ const {
+ stores: { api, error, location }
+ } = this.props;
if (error) {
return (
-
+
+
+
);
}
- if (!api || !currentLocation) {
- return ;
+ if (!api || !location.gps) {
+ return ;
}
return (
- {showVideo && (
-
- )}
);
};
@@ -200,13 +107,5 @@ export class Screens extends Component {
const styles = StyleSheet.create({
container: {
flexGrow: 1
- },
- video: {
- bottom: 0,
- height: Dimensions.get('screen').height,
- position: 'absolute',
- right: 0,
- width: Dimensions.get('screen').width,
- zIndex: -1
}
});
diff --git a/App/Screens/Search/Item/Item.js b/App/Screens/Search/Item/Item.js
index 2e2f77f0..5d8f89de 100644
--- a/App/Screens/Search/Item/Item.js
+++ b/App/Screens/Search/Item/Item.js
@@ -1,13 +1,13 @@
-// 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 { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import pinIcon from '../../../../assets/images/location.png';
import * as theme from '../../../utils/theme';
-export class Item extends Component {
+export class Item extends PureComponent {
handleClick = () => {
const {
item: { city, country, county, _geoloc, locale_names: localeNames },
diff --git a/App/Screens/Search/Item/index.js b/App/Screens/Search/Item/index.js
index 163fbf71..5756bcb7 100644
--- a/App/Screens/Search/Item/index.js
+++ b/App/Screens/Search/Item/index.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
export * from './Item';
diff --git a/App/Screens/Search/Search.js b/App/Screens/Search/Search.js
index 892a833d..e11c686f 100644
--- a/App/Screens/Search/Search.js
+++ b/App/Screens/Search/Search.js
@@ -1,10 +1,11 @@
-// 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 axios from 'axios';
import { Constants } from 'expo';
-import { FlatList, Modal, StyleSheet, Text, View } from 'react-native';
+import { FlatList, StyleSheet, Text, View } from 'react-native';
+import { inject, observer } from 'mobx-react';
import retry from 'async-retry';
import { BackButton } from '../../components/BackButton';
@@ -20,6 +21,8 @@ const algoliaUrls = [
'https://places-3.algolianet.com'
];
+@inject('stores')
+@observer
export class Search extends Component {
state = {
hasErrors: false, // Error from algolia
@@ -35,7 +38,12 @@ export class Search extends Component {
}
fetchResults = async search => {
- const { gps } = this.props;
+ const {
+ stores: {
+ location: { gps }
+ }
+ } = this.props;
+
try {
this.setState({ loading: true });
await retry(
@@ -90,37 +98,35 @@ export class Search extends Component {
};
handleItemClick = item => {
- this.setState({ search: '' });
- this.props.onLocationChanged(item);
+ // Reset everything when we choose a new location.
+ this.props.stores.location.setCurrent(item);
+ this.props.stores.setError(false);
+ this.props.stores.setApi(undefined);
};
render () {
- const { onRequestClose, ...rest } = this.props;
+ const { navigation } = this.props;
const { hits, search } = this.state;
return (
-
-
-
-
- objectID}
- ListEmptyComponent={
- {this.renderInfoText()}
- }
- renderItem={this.renderItem}
- style={styles.list}
- />
-
-
+
+
+
+ objectID}
+ ListEmptyComponent={
+ {this.renderInfoText()}
+ }
+ renderItem={this.renderItem}
+ style={styles.list}
+ />
+
);
}
@@ -145,9 +151,8 @@ const styles = StyleSheet.create({
marginVertical: 18
},
container: {
- ...theme.fullScreen,
- ...theme.modal,
- backgroundColor: 'white'
+ backgroundColor: 'white',
+ flexGrow: 1
},
list: {
flex: 1
@@ -155,7 +160,7 @@ const styles = StyleSheet.create({
noResults: {
...theme.text,
...theme.withPadding,
- marginTop: 22
+ marginTop: theme.spacing.normal
},
separator: {
backgroundColor: '#D2D2D2',
diff --git a/App/Screens/Search/SearchHeader/SearchHeader.js b/App/Screens/Search/SearchHeader/SearchHeader.js
index bf042426..3a54333a 100644
--- a/App/Screens/Search/SearchHeader/SearchHeader.js
+++ b/App/Screens/Search/SearchHeader/SearchHeader.js
@@ -1,77 +1,29 @@
-// 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,
- TextInput,
- TouchableHighlight,
- View
-} from 'react-native';
+import React, { PureComponent } from 'react';
+import { Image, StyleSheet, TextInput } from 'react-native';
+import { Banner } from '../../../components/Banner';
import searchIcon from '../../../../assets/images/search.png';
import * as theme from '../../../utils/theme';
-export class SearchHeader extends Component {
- handleClick = () => {
- const {
- item: { city, country, county, _geoloc, locale_names: localeNames },
- onClick
- } = this.props;
- onClick({
- latitude: _geoloc.lat,
- longitude: _geoloc.lng,
- name: [
- localeNames[0],
- city,
- county && county.length ? county[0] : null,
- country
- ]
- .filter(_ => _)
- .join(', ')
- });
- };
-
+export class SearchHeader extends PureComponent {
render () {
- const {
- asTouchable,
- elevated,
- onChangeSearch,
- onClick,
- search,
- style,
- ...rest
- } = this.props;
- const Wrapper = asTouchable ? TouchableHighlight : View;
-
+ const { onChangeSearch, search } = this.props;
return (
-
-
-
-
-
-
+
+
+
+
);
}
}
diff --git a/App/Screens/Search/SearchHeader/index.js b/App/Screens/Search/SearchHeader/index.js
index d512c929..c5d8542c 100644
--- a/App/Screens/Search/SearchHeader/index.js
+++ b/App/Screens/Search/SearchHeader/index.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
export * from './SearchHeader';
diff --git a/App/Screens/Search/index.js b/App/Screens/Search/index.js
index 4e768ef2..3f12fbc0 100644
--- a/App/Screens/Search/index.js
+++ b/App/Screens/Search/index.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
export * from './Search';
diff --git a/App/Screens/index.js b/App/Screens/index.js
index f4c60a6a..7da7a6a9 100644
--- a/App/Screens/index.js
+++ b/App/Screens/index.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
export * from './Screens';
diff --git a/App/components/BackButton/BackButton.js b/App/components/BackButton/BackButton.js
index 4d89e566..89a9fb75 100644
--- a/App/components/BackButton/BackButton.js
+++ b/App/components/BackButton/BackButton.js
@@ -1,13 +1,13 @@
-// 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 { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import backIcon from '../../../assets/images/back.png';
import * as theme from '../../utils/theme';
-export class BackButton extends Component {
+export class BackButton extends PureComponent {
onClick = () => this.props.onClick();
render () {
@@ -34,9 +34,7 @@ const styles = StyleSheet.create({
flexDirection: 'row'
},
backText: {
- color: theme.secondaryTextColor,
- fontFamily: 'gotham-book',
- fontSize: 13,
+ ...theme.text,
marginLeft: 9
}
});
diff --git a/App/components/BackButton/index.js b/App/components/BackButton/index.js
index de014319..4eae2378 100644
--- a/App/components/BackButton/index.js
+++ b/App/components/BackButton/index.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
export * from './BackButton';
diff --git a/App/components/Banner/Banner.js b/App/components/Banner/Banner.js
new file mode 100644
index 00000000..9140f3a2
--- /dev/null
+++ b/App/components/Banner/Banner.js
@@ -0,0 +1,50 @@
+// Copyright (c) 2018, Amaury Martiny
+// SPDX-License-Identifier: GPL-3.0
+
+import React from 'react';
+import { StyleSheet, TouchableHighlight, View } from 'react-native';
+
+import * as theme from '../../utils/theme';
+
+export const Banner = ({
+ asTouchable,
+ children,
+ elevated,
+ onClick,
+ shadowPosition,
+ style
+}) => {
+ const Wrapper = asTouchable ? TouchableHighlight : View;
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ backgroundColor: theme.primaryColor,
+ zIndex: 1
+ },
+ content: {
+ ...theme.withPadding,
+ alignItems: 'center',
+ flexDirection: 'row',
+ height: 48
+ }
+});
diff --git a/App/components/Banner/index.js b/App/components/Banner/index.js
new file mode 100644
index 00000000..68de8c5a
--- /dev/null
+++ b/App/components/Banner/index.js
@@ -0,0 +1,4 @@
+// Copyright (c) 2018, Amaury Martiny
+// SPDX-License-Identifier: GPL-3.0
+
+export * from './Banner';
diff --git a/App/components/CurrentLocation/CurrentLocation.js b/App/components/CurrentLocation/CurrentLocation.js
new file mode 100644
index 00000000..3455d4ef
--- /dev/null
+++ b/App/components/CurrentLocation/CurrentLocation.js
@@ -0,0 +1,82 @@
+// Copyright (c) 2018, Amaury Martiny
+// SPDX-License-Identifier: GPL-3.0
+
+import React, { Component } from 'react';
+import axios from 'axios';
+import { Constants } from 'expo';
+import { inject, observer } from 'mobx-react';
+import { StyleSheet, Text } from 'react-native';
+
+import * as theme from '../../utils/theme';
+
+@inject('stores')
+@observer
+export class CurrentLocation extends Component {
+ state = {
+ locationName: 'Fetching...'
+ };
+
+ async componentDidMount () {
+ const {
+ stores: { api, location }
+ } = this.props;
+
+ // If our currentLocation already has a name (from Algolia), then we don't
+ // need Google Geocoding for the name
+ if (location.name) {
+ this.setState({ locationName: location.name.toUpperCase() });
+ return;
+ }
+
+ try {
+ const { data } = await axios.get(
+ `https://us1.locationiq.com/v1/reverse.php?key=${
+ Constants.manifest.extra.locationIqKey
+ }&lat=${location.current.latitude}&lon=${
+ location.current.longitude
+ }&format=json`
+ );
+
+ // If we got data from the Google Geocoding service, then we use that one
+ if (!data || !data.address || !data.display_name) {
+ throw new Error('No data from LocationIQ.');
+ }
+
+ // We format the formatted_address to remove postal code and street number for privacy reasons
+ const postalCode = data.address.postcode;
+ const streetNumber = data.address.house_number;
+
+ this.setState({
+ locationName: data.display_name
+ .replace(postalCode, '')
+ .replace(streetNumber, '')
+ .replace(/^,/, '') // Remove starting comma
+ .replace(', ,', ',') // Remove unnecessary commas
+ .replace(/ +/g, ' ') // Remove double spaces
+ .replace(' ,', ',') // Self-explanatory
+ .trim()
+ .toUpperCase()
+ });
+ } catch (error) {
+ // Show AQI station name if we don't have reverse geocoding data
+ this.setState({ locationName: api.city.name.toUpperCase() });
+ }
+ }
+
+ render () {
+ const { stores, style, ...rest } = this.props;
+ const { locationName } = this.state;
+
+ return (
+
+ {locationName}
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ title: {
+ ...theme.title
+ }
+});
diff --git a/App/components/CurrentLocation/index.js b/App/components/CurrentLocation/index.js
new file mode 100644
index 00000000..5b661039
--- /dev/null
+++ b/App/components/CurrentLocation/index.js
@@ -0,0 +1,4 @@
+// Copyright (c) 2018, Amaury Martiny
+// SPDX-License-Identifier: GPL-3.0
+
+export * from './CurrentLocation';
diff --git a/App/components/Footer/About/About.js b/App/components/Footer/About/About.js
deleted file mode 100644
index 064a79a8..00000000
--- a/App/components/Footer/About/About.js
+++ /dev/null
@@ -1,227 +0,0 @@
-// Copyright (c) 2018, Amaury Martiny and the Shoot! I Smoke contributors
-// SPDX-License-Identifier: GPL-3.0
-
-import React, { Component } from 'react';
-import { Constants } from 'expo';
-import {
- Image,
- Linking,
- Modal,
- Platform,
- ScrollView,
- StyleSheet,
- Text,
- View
-} from 'react-native';
-
-import cigarette from '../../../../assets/images/cigarette.png';
-import * as theme from '../../../utils/theme';
-import { BackButton } from '../../BackButton';
-
-export class About extends Component {
- 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 { onRequestClose, ...rest } = this.props;
-
- return (
-
-
-
-
-
-
- About
-
- 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'}.
-
-
-
-
-
-
- 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.
-
-
-
- (1){' '}
-
- http://berkeleyearth.org/air-pollution-and-cigarette-equivalence/
-
-
-
-
- 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({
- aboutTitle: {
- ...theme.title,
- fontSize: 36,
- marginBottom: 12
- },
- articleLink: {
- ...theme.text,
- fontSize: 8
- },
- backButton: {
- marginBottom: 22,
- marginTop: 22
- },
- 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: 16,
- position: 'absolute'
- },
- container: {
- ...theme.fullScreen,
- ...theme.modal,
- flex: 1,
- flexDirection: 'column',
- justifyContent: 'space-between'
- },
- credits: {
- marginBottom: 22,
- marginTop: 22
- },
- creditsTitle: {
- ...theme.title,
- fontSize: 20,
- marginBottom: 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'
- }
- })
- },
- scrollView: {
- flexDirection: 'column',
- justifyContent: 'flex-start'
- },
- 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/components/Footer/About/index.js b/App/components/Footer/About/index.js
deleted file mode 100644
index 85e85dd7..00000000
--- a/App/components/Footer/About/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// Copyright (c) 2018, Amaury Martiny and the Shoot! I Smoke contributors
-// SPDX-License-Identifier: GPL-3.0
-
-export * from './About';
diff --git a/App/components/Footer/Footer.js b/App/components/Footer/Footer.js
index ffa17c81..ce8cfa5c 100644
--- a/App/components/Footer/Footer.js
+++ b/App/components/Footer/Footer.js
@@ -1,13 +1,13 @@
-// 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 { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
-import { About } from './About';
+import { About } from '../../Screens/About';
import * as theme from '../../utils/theme';
-export class Footer extends Component {
+export class Footer extends PureComponent {
static defaultProps = {
text: 'Click to understand how we did the math.'
};
@@ -36,8 +36,8 @@ export class Footer extends Component {
const styles = StyleSheet.create({
container: {
- marginBottom: 22,
- marginTop: 22
+ marginBottom: theme.spacing.normal,
+ marginTop: theme.spacing.normal
},
link: {
...theme.text,
diff --git a/App/components/Footer/index.js b/App/components/Footer/index.js
index 01a77a95..c0a9d3d5 100644
--- a/App/components/Footer/index.js
+++ b/App/components/Footer/index.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
export * from './Footer';
diff --git a/App/components/Header/Header.js b/App/components/Header/Header.js
deleted file mode 100644
index 88964502..00000000
--- a/App/components/Header/Header.js
+++ /dev/null
@@ -1,151 +0,0 @@
-// Copyright (c) 2018, Amaury Martiny and the Shoot! I Smoke contributors
-// SPDX-License-Identifier: GPL-3.0
-
-import React, { Component } from 'react';
-import axios from 'axios';
-import { Constants } from 'expo';
-import haversine from 'haversine';
-import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
-
-import { BackButton } from '../BackButton';
-import changeLocation from '../../../assets/images/changeLocation.png';
-import { getCorrectLatLng } from '../../utils/getCorrectLatLng';
-import * as theme from '../../utils/theme';
-
-export class Header extends Component {
- static defaultProps = {
- showChangeLocation: false
- };
-
- state = {
- locationName: 'FETCHING...'
- };
-
- async componentDidMount () {
- const { api, currentLocation } = this.props;
-
- // If our currentLocation already has a name (from algolia), then we don't
- // need Google Geocoding for the name
- if (currentLocation.name) {
- this.setState({ locationName: currentLocation.name.toUpperCase() });
- return;
- }
-
- try {
- const { data } = await axios.get(
- `https://us1.locationiq.com/v1/reverse.php?key=${
- Constants.manifest.extra.locationIqKey
- }&lat=${currentLocation.latitude}&lon=${
- currentLocation.longitude
- }&format=json`
- );
-
- // If we got data from the Google Geocoding service, then we use that one
- if (!data || !data.address || !data.display_name) {
- throw new Error('No data from LocationIQ.');
- }
-
- // We format the formatted_address to remove postal code and street number for privacy reasons
- const postalCode = data.address.postcode;
- const streetNumber = data.address.house_number;
-
- this.setState({
- locationName: data.display_name
- .replace(postalCode, '')
- .replace(streetNumber, '')
- .replace(/^,/, '') // Remove starting comma
- .replace(', ,', ',') // Remove unnecessary commas
- .replace(/ +/g, ' ') // Remove double spaces
- .replace(' ,', ',') // Self-explanatory
- .trim()
- .toUpperCase()
- });
- } catch (error) {
- this.setState({ locationName: api.city.name.toUpperCase() });
- }
- }
-
- render () {
- const {
- api,
- currentLocation,
- elevated,
- onBackClick,
- onChangeLocationClick,
- onClick,
- showBackButton,
- showChangeLocation,
- style
- } = this.props;
- const { locationName } = this.state;
- const distance = Math.round(
- haversine(
- currentLocation,
- getCorrectLatLng(currentLocation, {
- latitude: api.city.geo[0],
- longitude: api.city.geo[1]
- })
- )
- );
-
- return (
-
- {showBackButton && (
-
- )}
-
-
-
- {locationName}
-
- Distance to Air Quality Station: {distance}
- km
-
-
- {showChangeLocation && (
-
-
-
- )}
-
-
- );
- }
-}
-
-const styles = StyleSheet.create({
- backButton: {
- marginBottom: 22
- },
- changeLocation: {
- marginRight: 5
- },
- container: {
- paddingBottom: 15,
- paddingHorizontal: 17,
- paddingTop: 18
- },
- content: {
- alignItems: 'center',
- flexDirection: 'row',
- justifyContent: 'space-between'
- },
- subtitle: {
- ...theme.text,
- marginTop: 11
- },
- title: {
- ...theme.title,
- fontSize: 15
- }
-});
diff --git a/App/components/Header/index.js b/App/components/Header/index.js
deleted file mode 100644
index 31203ad4..00000000
--- a/App/components/Header/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// Copyright (c) 2018, Amaury Martiny and the Shoot! I Smoke contributors
-// SPDX-License-Identifier: GPL-3.0
-
-export * from './Header';
diff --git a/App/index.js b/App/index.js
index 0faa3e4f..b320247d 100644
--- a/App/index.js
+++ b/App/index.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
import { App } from './App';
diff --git a/App/stores/api.js b/App/stores/api.js
new file mode 100644
index 00000000..479278f6
--- /dev/null
+++ b/App/stores/api.js
@@ -0,0 +1,34 @@
+// Copyright (c) 2018, Amaury Martiny
+// SPDX-License-Identifier: GPL-3.0
+
+import { types } from 'mobx-state-tree';
+
+export const ApiStore = types.maybe(
+ types.model('ApiStore', {
+ aqi: types.number,
+ attributions: types.array(
+ types.model({
+ name: types.string,
+ url: types.maybe(types.string)
+ })
+ ),
+ city: types.model({
+ geo: types.array(types.number),
+ name: types.string,
+ url: types.maybe(types.string)
+ }),
+ dominentpol: types.string,
+ iaqi: types.map(
+ types.model({
+ v: types.number
+ })
+ ),
+ idx: types.number,
+ rawPm25: types.number,
+ time: types.model({
+ s: types.maybe(types.string),
+ tz: types.maybe(types.string),
+ v: types.number
+ })
+ })
+);
diff --git a/App/stores/error.js b/App/stores/error.js
new file mode 100644
index 00000000..fb6019e2
--- /dev/null
+++ b/App/stores/error.js
@@ -0,0 +1,6 @@
+// Copyright (c) 2018, Amaury Martiny
+// SPDX-License-Identifier: GPL-3.0
+
+import { types } from 'mobx-state-tree';
+
+export const ErrorStore = types.boolean;
diff --git a/App/stores/index.js b/App/stores/index.js
new file mode 100644
index 00000000..0475dcd9
--- /dev/null
+++ b/App/stores/index.js
@@ -0,0 +1,59 @@
+// Copyright (c) 2018, Amaury Martiny
+// SPDX-License-Identifier: GPL-3.0
+
+import haversine from 'haversine';
+import { types } from 'mobx-state-tree';
+
+import { ApiStore } from './api';
+import { ErrorStore } from './error';
+import { getCorrectLatLng } from '../utils/getCorrectLatLng';
+import { LocationStore } from './location';
+import { pm25ToCigarettes } from '../utils/pm25ToCigarettes';
+
+// Above this distance, we consider the station too far from the user
+export const MAX_DISTANCE_TO_STATION = 15;
+
+export const RootStore = types
+ .model('RootStore', {
+ api: ApiStore,
+ error: ErrorStore,
+ location: LocationStore
+ })
+ .views(self => ({
+ get cigarettes () {
+ if (!self.api) {
+ return 0;
+ }
+ return pm25ToCigarettes(self.api.rawPm25);
+ },
+ get distanceToStation () {
+ if (!self.api || !self.location.current) {
+ return 0;
+ }
+
+ return Math.round(
+ haversine(
+ self.location.current,
+ getCorrectLatLng(self.location.current, {
+ latitude: self.api.city.geo[0],
+ longitude: self.api.city.geo[1]
+ })
+ )
+ );
+ }
+ }))
+ .views(self => ({
+ get isStationTooFar () {
+ return self.distanceToStation > MAX_DISTANCE_TO_STATION;
+ }
+ }))
+ .actions(self => ({
+ setApi (newApi) {
+ self.api = newApi;
+ },
+ setError (newError) {
+ // TODO Add sentry
+ // https://github.com/amaurymartiny/shoot-i-smoke/issues/22
+ self.error = newError;
+ }
+ }));
diff --git a/App/stores/location.js b/App/stores/location.js
new file mode 100644
index 00000000..0830c10d
--- /dev/null
+++ b/App/stores/location.js
@@ -0,0 +1,30 @@
+// Copyright (c) 2018, Amaury Martiny
+// SPDX-License-Identifier: GPL-3.0
+
+import { types } from 'mobx-state-tree';
+
+const location = name =>
+ types.maybe(
+ types.model(name, {
+ latitude: types.number,
+ longitude: types.number,
+ name: types.maybe(types.string)
+ })
+ );
+
+const current = location('CurrentStore');
+const gps = location('ApiStore');
+
+export const LocationStore = types
+ .model('LocationStore', {
+ current,
+ gps
+ })
+ .actions(self => ({
+ setCurrent (newCurrent) {
+ self.current = newCurrent;
+ },
+ setGps (newGps) {
+ self.gps = newGps;
+ }
+ }));
diff --git a/App/utils/dataSources/aqicn.js b/App/utils/dataSources/aqicn.js
index f8e19974..28807a2e 100644
--- a/App/utils/dataSources/aqicn.js
+++ b/App/utils/dataSources/aqicn.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
import axios from 'axios';
@@ -85,10 +85,13 @@ export const aqicn = async ({ latitude, longitude }) => {
}
return {
- pm25: response.data.iaqi.pm25.v,
- city: {
- name: response.data.city.name,
- geo: [+response.data.city.geo[0], +response.data.city.geo[1]]
- }
+ aqi: response.data.aqi,
+ attributions: response.data.attributions,
+ city: response.data.city,
+ dominentpol: response.data.dominentpol,
+ iaqi: response.data.iaqi,
+ idx: response.data.idx,
+ rawPm25: response.data.iaqi.pm25.v, // TODO Find the real raw value https://github.com/amaurymartiny/shoot-i-smoke/issues/46
+ time: response.data.time
};
};
diff --git a/App/utils/dataSources/index.js b/App/utils/dataSources/index.js
index 52697ba4..8cf2b748 100644
--- a/App/utils/dataSources/index.js
+++ b/App/utils/dataSources/index.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
export * from './aqicn';
diff --git a/App/utils/dataSources/windWaqi.js b/App/utils/dataSources/windWaqi.js
index 4a07511e..1d661bc8 100644
--- a/App/utils/dataSources/windWaqi.js
+++ b/App/utils/dataSources/windWaqi.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
import axios from 'axios';
@@ -42,8 +42,20 @@ export const windWaqi = async ({ latitude, longitude }) => {
}
return {
- pm25: data.v,
- city: { geo: [+data.geo[0], +data.geo[1]], name: data.nlo }
+ aqi: +data.v,
+ attributions: [],
+ city: { geo: [+data.geo[0], +data.geo[1]], name: data.nlo },
+ dominentpol: data.pol,
+ iaqi: {
+ [data.pol]: {
+ v: +data.v
+ }
+ },
+ idx: +data.x,
+ rawPm25: +data.v, // TODO Find the real raw value https://github.com/amaurymartiny/shoot-i-smoke/issues/46
+ time: {
+ v: data.t
+ }
};
} else {
throw new Error(response);
diff --git a/App/utils/getCorrectLatLng.js b/App/utils/getCorrectLatLng.js
index 40954c60..179ff271 100644
--- a/App/utils/getCorrectLatLng.js
+++ b/App/utils/getCorrectLatLng.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
/**
diff --git a/App/utils/pm25ToCigarettes.js b/App/utils/pm25ToCigarettes.js
index 338dbe55..b64caff5 100644
--- a/App/utils/pm25ToCigarettes.js
+++ b/App/utils/pm25ToCigarettes.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
/**
@@ -8,4 +8,4 @@
* @see http://berkeleyearth.org/air-pollution-and-cigarette-equivalence/
* @param {Float} api - The api object returned by the WAQI api.
*/
-export const pm25ToCigarettes = pm25 => pm25 / 22;
+export const pm25ToCigarettes = rawPm25 => rawPm25 / 22;
diff --git a/App/utils/theme.js b/App/utils/theme.js
index 354132cc..6c54cf17 100644
--- a/App/utils/theme.js
+++ b/App/utils/theme.js
@@ -1,31 +1,39 @@
-// Copyright (c) 2018, Amaury Martiny and the Shoot! I Smoke contributors
+// Copyright (c) 2018, Amaury Martiny
// SPDX-License-Identifier: GPL-3.0
import { Constants } from 'expo';
import { Platform } from 'react-native';
export const backgroundColor = '#FAFAFC';
+export const boldFont = 'gotham-black';
export const iconBackgroundColor = '#EBE7DD';
+export const normalFont = 'gotham-book';
export const primaryColor = '#F8A65D';
export const textColor = '#414248';
export const secondaryTextColor = '#7B7D88';
+export const spacing = {
+ tiny: 6,
+ small: 12,
+ normal: 18,
+ big: 36
+};
-export const elevatedLevel1 = {
+export const elevatedLevel1 = position => ({
elevation: 2,
shadowColor: 'black',
- shadowOffset: { width: 0, height: 2 },
+ shadowOffset: { width: 0, height: position === 'top' ? -2 : 2 },
shadowOpacity: 0.2,
shadowRadius: 2
-};
+});
-export const elevatedLevel2 = {
+export const elevatedLevel2 = position => ({
elevation: 10,
shadowColor: 'black',
- shadowOffset: { width: 0, height: 9 },
+ shadowOffset: { width: 0, height: position === 'top' ? -9 : 9 },
shadowOpacity: 0.4,
shadowRadius: 9,
zIndex: 100
-};
+});
export const fullScreen = {
backgroundColor,
@@ -35,31 +43,16 @@ export const fullScreen = {
export const link = {
color: primaryColor,
- fontFamily: 'gotham-book',
+ fontFamily: normalFont,
textDecorationLine: 'underline'
};
-export const modal = Platform.select({
- android: {
- marginTop: -Constants.statusBarHeight // On Android the modal only goes up until the status bar
- }
-});
-
-export const paragraph = Platform.select({
- android: {
- lineHeight: 28
- },
- ios: {
- lineHeight: 20
- }
-});
-
/**
* Big text with "Sh*t! I smoked...""
*/
export const shitText = {
color: textColor,
- fontFamily: 'gotham-black',
+ fontFamily: boldFont,
fontSize: 48
};
@@ -68,27 +61,31 @@ export const shitText = {
*/
export const text = {
color: secondaryTextColor,
- fontFamily: 'gotham-book',
+ fontFamily: normalFont,
fontSize: 12,
- letterSpacing: 0.22,
+ letterSpacing: 0.85,
+ lineHeight: 16,
textAlign: 'justify'
};
export const title = {
letterSpacing: 2,
+ lineHeight: 21,
color: textColor,
- fontFamily: 'gotham-black'
+ fontFamily: boldFont,
+ fontSize: 15
};
export const withPadding = {
- paddingHorizontal: 19
+ paddingHorizontal: spacing.normal
};
export const bigButton = {
backgroundColor: primaryColor,
- borderRadius: 22,
- paddingHorizontal: 22,
- paddingVertical: 11
+ borderRadius: 24,
+ height: 48,
+ paddingHorizontal: 24,
+ paddingVertical: 12
};
export const bigButtonText = {
@@ -96,11 +93,12 @@ export const bigButtonText = {
color: 'white',
...Platform.select({
android: {
- fontSize: 15
+ fontSize: 14
},
ios: {
fontSize: 14
}
}),
+ lineHeight: 24,
textAlign: 'center'
};
diff --git a/README.md b/README.md
index 44797c6d..cfc1da4c 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,13 @@ Know how many cigarettes you smoke based on the pollution of your location.
## Screenshots
-![screenshot-1](https://lh3.googleusercontent.com/BLVtUTguTcE7J1oeovfQhu1OI7jChczWv-evW2QgYlD8Dcv-66oGe4Th6O_soGP9SPA=w720-h310) ![screenshot-2](https://lh3.googleusercontent.com/XJTcPDB211FAJVFRpxxePlItSUy4rrZepOmRVZlM9kiF6DIorSOSfaFH1-0tSsQauw=w720-h310) ![screenshot-3](https://lh3.googleusercontent.com/j5-atGUl2UlY7UOF0x3dLA-qR9QWW8IdGmA8ZsBY06_W-W3uMDYzCprt5E2AdGdPiA=w720-h310)
+
+
+
+
+
+
+
## Try it on Expo
@@ -51,11 +57,11 @@ cp app.example.json app.json # Replace the API keys placeholders with your own t
yarn start
```
-This app is created with Expo, using React Native. When you run `yarn start`, the packager will show, and you can either:
+This app is created with Expo, using React Native. When you run `yarn start`, the Expo packager will show, and you can either:
- install the Expo app, scan the displayed QR code, and run the app on your mobile phone directly.
-- press `a` to open the Android simulator.
-- press `i` to open the iOS simulator.
+- open the Android simulator.
+- open the iOS simulator.
All the code lives in the `App/` folder. The app itself is pretty small, so it should be fairly easy to navigate through the files.
diff --git a/app.example.json b/app.example.json
index 99ad26c9..9e58801c 100644
--- a/app.example.json
+++ b/app.example.json
@@ -7,7 +7,7 @@
"icon": "assets/logos/android/playstore-icon.png",
"package": "com.shitismoke.app",
"permissions": ["ACCESS_FINE_LOCATION"],
- "versionCode": 4
+ "versionCode": 5
},
"assetBundlePatterns": ["assets/fonts/*"],
"extra": {
@@ -20,7 +20,7 @@
"githubUrl": "https://github.com/amaurymartiny/shoot-i-smoke",
"icon": "assets/logos/ios/iTunesArtwork@3x.png",
"ios": {
- "buildNumber": "1.1.8",
+ "buildNumber": "1.2.0",
"bundleIdentifier": "com.shitismoke.app",
"config": {
"googleMapsApiKey": "YOUR_KEY"
@@ -40,6 +40,6 @@
"backgroundColor": "#ebe7dd",
"image": "assets/logos/splash.png"
},
- "version": "1.1.8"
+ "version": "1.2.0"
}
}
diff --git a/assets/images/warning.png b/assets/images/warning.png
new file mode 100755
index 00000000..e4b5a762
Binary files /dev/null and b/assets/images/warning.png differ
diff --git a/assets/images/warning@2x.png b/assets/images/warning@2x.png
new file mode 100755
index 00000000..ae8291be
Binary files /dev/null and b/assets/images/warning@2x.png differ
diff --git a/assets/images/warning@3x.png b/assets/images/warning@3x.png
new file mode 100755
index 00000000..926e3544
Binary files /dev/null and b/assets/images/warning@3x.png differ
diff --git a/package.json b/package.json
index c3dfa6e4..d0d917ca 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "shoot-i-smoke",
- "version": "1.1.8",
+ "version": "1.2.0",
"main": "node_modules/expo/AppEntry.js",
"private": true,
"scripts": {
@@ -9,10 +9,15 @@
"test": "echo Skipped for now."
},
"dependencies": {
+ "@expo/vector-icons": "^8.0.0",
"async-retry": "^1.2.3",
"axios": "^0.18.0",
+ "date-fns": "^2.0.0-alpha.25",
"expo": "^30.0.0",
"haversine": "^1.1.0",
+ "mobx": "^5.6.0",
+ "mobx-react": "^5.3.6",
+ "mobx-state-tree": "^3.7.1",
"react": "16.3.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-30.0.0.tar.gz",
"react-navigation": "^2.13.0",
diff --git a/yarn.lock b/yarn.lock
index e9d476db..19b6fee0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -577,6 +577,14 @@
lodash "^4.17.4"
react-native-vector-icons "4.5.0"
+"@expo/vector-icons@^8.0.0":
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/@expo/vector-icons/-/vector-icons-8.0.0.tgz#29c91778aee02b92b045914bbed8eec299a84e5e"
+ integrity sha512-DyVGGguxCsT0aMQrWAbS40AmlK648TehEEFkcibLwYjle7kUNh7clgQwRnCxnmjHn8VltbXpNrJNor9BVIHMLQ==
+ dependencies:
+ lodash "^4.17.4"
+ react-native-vector-icons "6.0.0"
+
"@expo/websql@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@expo/websql/-/websql-1.0.1.tgz#fff0cf9c1baa1f70f9e1d658b7c39a420d9b10a9"
@@ -2042,6 +2050,11 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0:
shebang-command "^1.2.0"
which "^1.2.9"
+date-fns@^2.0.0-alpha.25:
+ version "2.0.0-alpha.25"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.25.tgz#651a5d1f59a01af6cf0371e39b2ae29df5d521ee"
+ integrity sha512-iQzJkHF0L4wah9Ae9PkvwemwFz6qmRLuNZcghmvf2t+ptLs1qXzONLiGtjmPQzL6+JpC01JjlTopY2AEy4NFAg==
+
debug-log@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debug-log/-/debug-log-1.0.1.tgz#2307632d4c04382b8df8a32f70b895046d52745f"
@@ -3338,6 +3351,13 @@ hoist-non-react-statics@^2.5.0:
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
+hoist-non-react-statics@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.1.0.tgz#42414ccdfff019cd2168168be998c7b3bd5245c0"
+ integrity sha512-MYcYuROh7SBM69xHGqXEwQqDux34s9tz+sCnxJmN18kgWh6JFdTw/5YdZtqsOdZJXddE/wUpCzfEdDrJj8p0Iw==
+ dependencies:
+ react-is "^16.3.2"
+
home-or-tmp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@@ -4337,6 +4357,24 @@ mkdirp@^0.5.0, mkdirp@^0.5.1:
dependencies:
minimist "0.0.8"
+mobx-react@^5.3.6:
+ version "5.3.6"
+ resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-5.3.6.tgz#0bead0173aed7d12ace4981b53c28db4d61de9ef"
+ integrity sha512-u0ehjkyMllnt+SQ/G5BACxwi4VkJOqDVauA7cL8UvDi4oLO1ID9Z5c/nudTdIAmtsDDhJSxW85mrgsA2Iv/56Q==
+ dependencies:
+ hoist-non-react-statics "^3.0.0"
+ react-lifecycles-compat "^3.0.2"
+
+mobx-state-tree@^3.7.1:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/mobx-state-tree/-/mobx-state-tree-3.7.1.tgz#161fc1b46224d3defa28b482914d3aa8e1b80ed7"
+ integrity sha512-XJHT4ldB2eoHBOqTsNLOsJHNsClSkAUwNWQm9njat28Md0ymQIpVPpcKywz9I8tbu2RoWlEm1/U4v6SeQEk/sA==
+
+mobx@^5.6.0:
+ version "5.6.0"
+ resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.6.0.tgz#e381c357e0ed7685a99a896455ecc66ff5ab84ee"
+ integrity sha512-xrA0tBnSMANXCDjS2W/6YTasesA8mkg9o+v/Bw/OcbXaRVE6/soVwDMWIh7A6TwMDY4tYQprQJZ5WQN1TRSb3A==
+
morgan@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.9.0.tgz#d01fa6c65859b76fcf31b3cb53a3821a311d8051"
@@ -4993,7 +5031,12 @@ react-devtools-core@3.1.0:
shell-quote "^1.6.1"
ws "^2.0.3"
-react-lifecycles-compat@^3, react-lifecycles-compat@^3.0.4:
+react-is@^16.3.2:
+ version "16.6.3"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.6.3.tgz#d2d7462fcfcbe6ec0da56ad69047e47e56e7eac0"
+ integrity sha512-u7FDWtthB4rWibG/+mFbVd5FvdI20yde86qKGx4lVUTWmPlSWQ4QxbBIrrs+HnXGbxOUlUzTAP/VDmvCwaP2yA==
+
+react-lifecycles-compat@^3, react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
@@ -5095,6 +5138,15 @@ react-native-vector-icons@4.5.0:
prop-types "^15.5.10"
yargs "^8.0.2"
+react-native-vector-icons@6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/react-native-vector-icons/-/react-native-vector-icons-6.0.0.tgz#3a7076dbe244ea94c6d5e92802a870e64a4283c5"
+ integrity sha512-uF3oWb3TV42uXi2apVOZHw9oy9Nr5SXDVwOo1umQWo/yYCrDzXyVfq14DzezgEbJ9jfc/yghBelj0agkXmOKlg==
+ dependencies:
+ lodash "^4.0.0"
+ prop-types "^15.6.2"
+ yargs "^8.0.2"
+
"react-native@https://github.com/expo/react-native/archive/sdk-30.0.0.tar.gz":
version "0.55.4"
resolved "https://github.com/expo/react-native/archive/sdk-30.0.0.tar.gz#17d148956ad91ab745fdca830ac3d193564fce5c"