diff --git a/design/Deighton AR Design.sketch b/design/Deighton AR Design.sketch index 8277f85..dd63102 100644 Binary files a/design/Deighton AR Design.sketch and b/design/Deighton AR Design.sketch differ diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index e949e02..ea1b81f 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + NSCameraUsageDescription $(PRODUCT_NAME) camera use - NSLocationWhenInUseUsageDescription - NSPhotoLibraryUsageDescription $(PRODUCT_NAME) photo use UILaunchStoryboardName @@ -52,6 +50,8 @@ UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown + NSLocationWhenInUseUsageDescription + $(PRODUCT_NAME) location use UIViewControllerBasedStatusBarAppearance diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 0c0ddba..476c6ab 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -5451,6 +5451,11 @@ "resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.0.2.tgz", "integrity": "sha512-5FYNC4kTi/YK86l+r8GQ0xgsSL2tleCQ5Yppu1+ARbnm2qGRmDoJTGSNsWBAWa8FP1ORyhMjxi18IlvSRKaI2g==" }, + "react-native-keyboard-spacer": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/react-native-keyboard-spacer/-/react-native-keyboard-spacer-0.4.1.tgz", + "integrity": "sha1-RvGKMgQyCYol6p+on1FD3SVNMy0=" + }, "react-native-maps": { "version": "0.20.1", "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-0.20.1.tgz", diff --git a/mobile/package.json b/mobile/package.json index c7c19c7..babe66d 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -26,6 +26,7 @@ "react-form-binder": "^1.2.0", "react-native": "^0.51.1", "react-native-iphone-x-helper": "^1.0.2", + "react-native-keyboard-spacer": "^0.4.1", "react-native-maps": "^0.20.1", "react-native-modal": "^5.4.0", "react-router-native": "^4.2.0", diff --git a/mobile/src/API.js b/mobile/src/API.js index c97589f..2009db2 100644 --- a/mobile/src/API.js +++ b/mobile/src/API.js @@ -44,7 +44,6 @@ class APIError extends Error { class API extends EventEmitter { constructor() { super() - // We don't know if the user has a valid token yet so assume they do and clean-up if they don't this.user = { pending: true } AsyncStorage.getItem(authTokenName).then((token) => { diff --git a/mobile/src/Auth/DefaultRoute.js b/mobile/src/Auth/DefaultRoute.js index 43d04d4..475a030 100644 --- a/mobile/src/Auth/DefaultRoute.js +++ b/mobile/src/Auth/DefaultRoute.js @@ -2,5 +2,6 @@ import React, { Fragment, Component } from 'react' import { Route, Redirect } from 'react-router-native' export const DefaultRoute = () => { - return ()} /> + // NOTE: When working on the app, change this to the page you are working on + return ()} /> } diff --git a/mobile/src/Home/Home.js b/mobile/src/Home/Home.js index 785d606..56290b8 100644 --- a/mobile/src/Home/Home.js +++ b/mobile/src/Home/Home.js @@ -1,5 +1,13 @@ import React from 'react' -import { StyleSheet, Text, TextInput, FlatList, Image, View, TouchableOpacity } from 'react-native' +import { + StyleSheet, + Text, + TextInput, + FlatList, + Image, + View, + TouchableOpacity, +} from 'react-native' import MapView, { Marker } from 'react-native-maps' import { Icon, Header } from '../ui' import { api } from '../API' @@ -66,20 +74,34 @@ export class Home extends React.Component { this.props.history.push('/arviewer') } + @autobind + handleMyLocationPress() { + navigator.geolocation.getCurrentPosition((info) => { + if (this.map) { + this.map.animateToCoordinate({latitude: info.coords.latitude, longitude: info.coords.longitude}) + } + }) + } + render() { return (
{ this.map = ref }} style={{ width: '100%', height: '50%', }} + showsUserLocation + showsBuildings={false} + showsTraffic={false} + showsIndoors={false} zoomControlEnabled initialRegion={{ latitude: 43.653908, longitude: -79.384293, latitudeDelta: 0.0922, - longitudeDelta: 0.0421, + longitudeDelta: 0.0922, }}> { data.map(marker => ( diff --git a/mobile/src/WorkItem/WorkItem.js b/mobile/src/WorkItem/WorkItem.js index 54dc9ac..3076f45 100644 --- a/mobile/src/WorkItem/WorkItem.js +++ b/mobile/src/WorkItem/WorkItem.js @@ -10,10 +10,17 @@ import { Platform, TouchableOpacity } from 'react-native' -import { Icon, Header, PhotoButton } from '../ui' import MapView, { Marker } from 'react-native-maps' import { FormBinder } from 'react-form-binder' -import { BoundInput, BoundButton } from '../ui' +import { + BoundInput, + BoundButton, + BoundHeader, + Icon, + Header, + PhotoButton, + BoundOptionStrip, +} from '../ui' import autobind from 'autobind-decorator' import { ifIphoneX, isIphoneX } from 'react-native-iphone-x-helper' @@ -39,12 +46,26 @@ const styles = StyleSheet.create({ } }) +const workItemOptions = [ + { value: 'workOrder', text: 'Work Order' }, + { value: 'inspection', text: 'Inspection' }, + { value: 'complaint', text: 'Complaint' } +] + export class WorkItem extends React.Component { static bindings = { + header: { + noValue: true, + isDisabled: (r) => (!(r.anyModified && r.allValid)), + }, location: { isValid: true, + isDisabled: true, }, details: { + isValid: (r, v) => (v !== ''), + }, + workItemType: { isValid: true, } } @@ -68,6 +89,22 @@ export class WorkItem extends React.Component { } } + @autobind + handleDonePress() { + + } + + @autobind + handleRegionChange(region) { + const { binder } = this.state + // NOTE: For some reason, object destructuring does not work here :( + const lon = region.longitude + const lat = region.latitude + + this.setState(binder.updateFieldValue('location', + `${Math.abs(lon).toFixed(4)}°${lon > 0 ? 'S' : 'N'}, ${Math.abs(lat).toFixed(4)}°${lat > 0 ? 'W' : 'E'}`)) + } + render() { const { binder } = this.state @@ -76,11 +113,21 @@ export class WorkItem extends React.Component { style={{ width: '100%', height: '100%' }} behavior='padding' keyboardVerticalOffset={Platform.select({ios: 0, android: -220})}> -
+ + + + + }} + onRegionChange={this.handleRegionChange}> + + @@ -103,7 +153,7 @@ export class WorkItem extends React.Component { - + { isIphoneX ? : null } diff --git a/mobile/src/ui/BoundButton.js b/mobile/src/ui/BoundButton.js index 4802683..cfc16cb 100644 --- a/mobile/src/ui/BoundButton.js +++ b/mobile/src/ui/BoundButton.js @@ -8,7 +8,6 @@ export class BoundButton extends React.Component { name: PropTypes.string.isRequired, title: PropTypes.string, binder: PropTypes.object.isRequired, - submit: PropTypes.string, onPress: PropTypes.func, width: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]), } @@ -16,7 +15,7 @@ export class BoundButton extends React.Component { constructor(props) { super(props) - let { name, binder } = this.props + const { name, binder } = this.props binder.addListener(name, this.updateValue) this.state = binder.getFieldState(name) @@ -40,7 +39,7 @@ export class BoundButton extends React.Component { } render() { - const { name, title, submit, width, onPress } = this.props + const { name, title, width, onPress } = this.props const { visible, disabled } = this.state if (!visible) { diff --git a/mobile/src/ui/BoundHeader.js b/mobile/src/ui/BoundHeader.js new file mode 100644 index 0000000..11d6c5e --- /dev/null +++ b/mobile/src/ui/BoundHeader.js @@ -0,0 +1,60 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Header } from '.' +import autobind from 'autobind-decorator' + +const headerButtonShape = { icon: PropTypes.string.isRequired, onPress: PropTypes.func } + +export class BoundHeader extends React.Component { + static propTypes = { + name: PropTypes.string.isRequired, + title: PropTypes.string, + leftButton: PropTypes.shape(headerButtonShape).isRequired, + rightButton: PropTypes.shape(headerButtonShape), + binder: PropTypes.object.isRequired, + } + + constructor(props) { + super(props) + + const { name, binder } = this.props + + binder.addListener(name, this.updateValue) + this.state = binder.getFieldState(name) + } + + @autobind + updateValue(e) { + this.setState(e.state) + } + + componentWillUnmount() { + this.props.binder.removeListener(this.props.name, this.updateValue) + } + + componentWillReceiveProps(nextProps) { + if (nextProps.binder !== this.props.binder) { + this.props.binder.removeListener(this.props.name, this.updateValue) + nextProps.binder.addListener(nextProps.name, this.updateValue) + this.setState(nextProps.binder.getFieldState(nextProps.name)) + } + } + + render() { + const { title, leftButton } = this.props + let { rightButton } = this.props + const { visible, disabled } = this.state + + if (!visible) { + rightButton = null + } + + return ( +
+ ) + } +} diff --git a/mobile/src/ui/BoundInput.js b/mobile/src/ui/BoundInput.js index 2d1d1a9..ac14906 100644 --- a/mobile/src/ui/BoundInput.js +++ b/mobile/src/ui/BoundInput.js @@ -10,7 +10,12 @@ export class BoundInput extends React.Component { label: PropTypes.string, binder: PropTypes.object.isRequired, password: PropTypes.bool, - placeholder: PropTypes.string + placeholder: PropTypes.string, + lines: PropTypes.number, + } + + static defaultProps = { + lines: 1, } constructor(props) { @@ -29,19 +34,30 @@ export class BoundInput extends React.Component { } render() { - const { label, password, name, placeholder, message } = this.props + const { label, password, name, placeholder, message, lines } = this.props const { visible, disabled, value, valid } = this.state if (!visible) { return null } - // TODO: Disabled - return ( {label} - 1} + numberOfLines={lines} + editable={!disabled} autoCapitalize='none' underlineColorAndroid='white' value={value} diff --git a/mobile/src/ui/Header.js b/mobile/src/ui/Header.js index 573d06a..b5fcd5b 100644 --- a/mobile/src/ui/Header.js +++ b/mobile/src/ui/Header.js @@ -9,17 +9,36 @@ const headerButtonShape = { icon: PropTypes.string.isRequired, onPress: PropType export class Header extends Component { static propTypes = { title: PropTypes.string.isRequired, - leftButton: PropTypes.shape(headerButtonShape), + leftButton: PropTypes.shape(headerButtonShape).isRequired, rightButton: PropTypes.shape(headerButtonShape), + disabled: PropTypes.bool, } static defaultProps = { rightButton: { icon: 'none' }, leftButton: { icon: 'none' }, + visible: true, } render() { - const { title, leftButton, rightButton } = this.props + const { title, leftButton, rightButton, disabled, visible } = this.props + let right = null + + if (!rightButton) { + right = ( + + ) + } else if (disabled) { + right = ( + + ) + } else { + right = ( + + + + ) + } return ( {title} - - - + {right} ) } diff --git a/mobile/src/ui/Icon.js b/mobile/src/ui/Icon.js index 218ad83..4834018 100644 --- a/mobile/src/ui/Icon.js +++ b/mobile/src/ui/Icon.js @@ -13,6 +13,7 @@ const images = { settings: require('./images/settings.png'), add: require('./images/add.png'), done: require('./images/done.png'), + target: require('./images/target.png'), } export class Icon extends Component { diff --git a/mobile/src/ui/OptionStrip.js b/mobile/src/ui/OptionStrip.js index 60b1bf2..305e4a7 100644 --- a/mobile/src/ui/OptionStrip.js +++ b/mobile/src/ui/OptionStrip.js @@ -23,7 +23,7 @@ export class OptionStrip extends Component { } componentWillReceiveProps(newProps) { - if (newProps.options !== props.options || newProps.value !== props.value) { + if (newProps.options !== this.props.options || newProps.value !== this.props.value) { this.setState({ selectedIndex: this.getSelectedIndex(newProps.options, newProps.value)}) } } diff --git a/mobile/src/ui/images/done.png b/mobile/src/ui/images/done.png index 5afda9f..8c07bb1 100644 Binary files a/mobile/src/ui/images/done.png and b/mobile/src/ui/images/done.png differ diff --git a/mobile/src/ui/images/target.png b/mobile/src/ui/images/target.png new file mode 100644 index 0000000..f9f30a4 Binary files /dev/null and b/mobile/src/ui/images/target.png differ diff --git a/mobile/src/ui/index.js b/mobile/src/ui/index.js index 4d00679..c65ea16 100644 --- a/mobile/src/ui/index.js +++ b/mobile/src/ui/index.js @@ -1,8 +1,9 @@ -export { BoundSwitch } from './BoundSwitch' -export { BoundInput } from './BoundInput' -export { BoundButton } from './BoundButton' -export { BoundOptionStrip } from './BoundOptionStrip' export { Icon } from './Icon' export { Header } from './Header' export { PhotoButton } from './PhotoButton' export { OptionStrip } from './OptionStrip' +export { BoundSwitch } from './BoundSwitch' +export { BoundInput } from './BoundInput' +export { BoundButton } from './BoundButton' +export { BoundOptionStrip } from './BoundOptionStrip' +export { BoundHeader } from './BoundHeader'