diff --git a/design/Deighton AR Design.sketch b/design/Deighton AR Design.sketch index c5afdb8..8277f85 100644 Binary files a/design/Deighton AR Design.sketch and b/design/Deighton AR Design.sketch differ diff --git a/mobile/src/API.js b/mobile/src/API.js index 0212c6d..c97589f 100644 --- a/mobile/src/API.js +++ b/mobile/src/API.js @@ -44,7 +44,8 @@ class APIError extends Error { class API extends EventEmitter { constructor() { super() - this.user = null + // 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) => { if (!token) { @@ -52,7 +53,6 @@ class API extends EventEmitter { } this.token = token - this.user = { pending: true } return this.who() }).then((user) => { this.user = user @@ -61,7 +61,7 @@ class API extends EventEmitter { }).catch(() => { AsyncStorage.removeItem(authTokenName) this.token = null - this.user = null + this.user = {} this.socket = null this.emit('logout') }) @@ -83,12 +83,6 @@ class API extends EventEmitter { // Filter the few massages that affect our cached user data to avoid a server round trip switch (eventName) { - case 'newThumbnailImage': - this.user.thumbnailImageId = eventData.imageId - break - case 'newProfileImage': - this.user.imageId = eventData.imageId - break default: // Nothing to see here... break @@ -219,7 +213,7 @@ class API extends EventEmitter { // Regardless of response, always logout in the client AsyncStorage.removeItem(authTokenName) this.token = null - this.user = null + this.user = {} this.disconnectSocket() this.emit('logout') } diff --git a/mobile/src/ARViewer/ARViewer.js b/mobile/src/ARViewer/ARViewer.js index 651f212..7b14058 100644 --- a/mobile/src/ARViewer/ARViewer.js +++ b/mobile/src/ARViewer/ARViewer.js @@ -1,43 +1,72 @@ import React from 'react' -import { StyleSheet, View } from 'react-native' +import { StyleSheet, View, TouchableHighlight, Image } from 'react-native' import { - ViroARSceneNavigator, ViroARScene, ViroARPlane, ViroBox, ViroText, ViroAmbientLight + ViroARSceneNavigator, + ViroARScene, + ViroARPlane, + ViroBox, + ViroNode, + ViroAmbientLight, + ViroSpotLight, + Viro3DObject, + ViroSurface, } from 'react-viro' import autobind from 'autobind-decorator' import backImage from './images/back.png' const styles = { - helloWorldTextStyle: { - fontFamily: 'Arial', - fontSize: 30, - color: '#ffffff', - textAlignVertical: 'center', - textAlign: 'center', - }, buttons : { height: 80, width: 80, }, } +const shapes = { + hardhat: { shape: require('./models/hardhat.obj'), materials: [ require('./models/hardhat.mtl') ] }, + question: { shape: require('./models/question.obj'), materials: [ require('./models/question.mtl') ] }, + clipboard: { shape: require('./models/question.obj'), materials: [ require('./models/clipboard.mtl') ] }, +} + class WorkItemSceneAR extends React.Component { constructor(props) { super(props) this.state = { - text : "Initializing AR..." + position: [0, .2, 0], + scale: [.2, .2, .2], } } render() { return ( - {this.setState({text : "Hello World!"})}}> + - - + + + + + ) @@ -62,7 +91,7 @@ export class ARViewer extends React.Component { apiKey='06F37B6A-74DA-4A83-965A-7DE2209A5C46' initialScene={{ scene: WorkItemSceneAR }} /> - + diff --git a/design/models/clipboard/clipboard.mtl b/mobile/src/ARViewer/models/clipboard.mtl similarity index 100% rename from design/models/clipboard/clipboard.mtl rename to mobile/src/ARViewer/models/clipboard.mtl diff --git a/design/models/clipboard/clipboard.obj b/mobile/src/ARViewer/models/clipboard.obj similarity index 100% rename from design/models/clipboard/clipboard.obj rename to mobile/src/ARViewer/models/clipboard.obj diff --git a/design/models/hardhat/hardhat.mtl b/mobile/src/ARViewer/models/hardhat.mtl similarity index 100% rename from design/models/hardhat/hardhat.mtl rename to mobile/src/ARViewer/models/hardhat.mtl diff --git a/design/models/hardhat/hardhat.obj b/mobile/src/ARViewer/models/hardhat.obj similarity index 100% rename from design/models/hardhat/hardhat.obj rename to mobile/src/ARViewer/models/hardhat.obj diff --git a/design/models/question/question.mtl b/mobile/src/ARViewer/models/question.mtl similarity index 100% rename from design/models/question/question.mtl rename to mobile/src/ARViewer/models/question.mtl diff --git a/design/models/question/question.obj b/mobile/src/ARViewer/models/question.obj similarity index 100% rename from design/models/question/question.obj rename to mobile/src/ARViewer/models/question.obj diff --git a/mobile/src/Activity/Activity.js b/mobile/src/Activity/Activity.js index b7cf767..76a431d 100644 --- a/mobile/src/Activity/Activity.js +++ b/mobile/src/Activity/Activity.js @@ -1,30 +1,142 @@ import React from 'react' -import { StyleSheet, View, TouchableOpacity, Image, ScrollView } from 'react-native' +import { + StyleSheet, + View, + Image, + ScrollView, + Text, + TextInput, + KeyboardAvoidingView, + Platform, + TouchableOpacity +} from 'react-native' +import MapView, { Marker } from 'react-native-maps' +import { FormBinder } from 'react-form-binder' +import { Icon, Header, PhotoButton, BoundInput, BoundButton, BoundOptionStrip } from '../ui' import autobind from 'autobind-decorator' +import { ifIphoneX, isIphoneX } from 'react-native-iphone-x-helper' + +const styles = StyleSheet.create({ + container: { + flexGrow: 1, + backgroundColor: '#DDDDDD', + }, + panel: { + width: '94%', + backgroundColor: 'white', + alignSelf: 'center', + marginTop: '3%', + shadowColor: 'gray', + shadowOffset: { width: 2, height: 2 }, + shadowRadius: 2, + shadowOpacity: .5, + padding: 10, + }, + label: { + fontSize: 14, + marginBottom: 4, + } +}) export class Activity extends React.Component { - static styles = StyleSheet.create({ - container: { - height: '100%', - width: '100%', - backgroundColor: '#AAAAAA', + static bindings = { + dateTime: { + isValid: true, }, - }) + location: { + isValid: true, + }, + details: { + isValid: true, + }, + resolution: { + isValid: true, + }, + notes: { + isValid: true, + }, + status: { + isValid: true, + } + } constructor(props) { super(props) + this.state = { + binder: new FormBinder({}, Activity.bindings), + messageModal: null, + } } - _handlePushButton(event) { - this.props.history.goBack() + @autobind + handleBackPress() { + const { history } = this.props + + if (history.length > 1) { + history.goBack() + } else { + history.replace('/home') + } } render() { + const { binder } = this.state + return ( - - - - + +
+ + + + + + + + + + + + + + + + + Pictures: + + + + + + + + + + + + { isIphoneX ? : null } + + ); } } diff --git a/mobile/src/Auth/DefaultRoute.js b/mobile/src/Auth/DefaultRoute.js index 38221a2..43d04d4 100644 --- a/mobile/src/Auth/DefaultRoute.js +++ b/mobile/src/Auth/DefaultRoute.js @@ -1,42 +1,6 @@ import React, { Fragment, Component } from 'react' -import { api } from '../API' import { Route, Redirect } from 'react-router-native' -import { View } from 'react-native' -import autobind from 'autobind-decorator' -export class DefaultRoute extends Component { - @autobind - updateComponent() { - this.forceUpdate() - } - - componentDidMount() { - api.addListener('login', this.updateComponent) - } - - componentWillUnmount() { - api.removeListener('login', this.updateComponent) - } - - render() { - const user = api.loggedInUser - let path = null - - if (user) { - if (!user.pending) { - path = '/home' - } - } else { - path = '/login' - } - - const { location } = this.props - - // Render a redirect or nothing until we finished logging on - return ( - ( - path ? : null - )} /> - ) - } +export const DefaultRoute = () => { + return ()} /> } diff --git a/mobile/src/Auth/ProtectedRoute.js b/mobile/src/Auth/ProtectedRoute.js index c3f9e51..b294496 100644 --- a/mobile/src/Auth/ProtectedRoute.js +++ b/mobile/src/Auth/ProtectedRoute.js @@ -17,25 +17,25 @@ export class ProtectedRoute extends React.Component { componentDidMount() { api.addListener('login', this.updateComponent) + api.addListener('logout', this.updateComponent) } componentWillUnmount() { api.removeListener('login', this.updateComponent) + api.removeListener('logout', this.updateComponent) } render(props) { const user = api.loggedInUser - if (user) { - if (user.pending) { - // The API might be in the middle of fetching the user information - return null - } else if (!this.props.admin || (this.props.admin && user.administrator)) { + if (user.pending) { + return null + } else { + if (!user._id || (this.props.admin && !user.administrator)) { + return + } else { return } } - - // TODO: Can add redirect back in here - see website - return } } diff --git a/mobile/src/Home/Home.js b/mobile/src/Home/Home.js index 42e27bd..785d606 100644 --- a/mobile/src/Home/Home.js +++ b/mobile/src/Home/Home.js @@ -5,6 +5,7 @@ import { Icon, Header } from '../ui' import { api } from '../API' import autobind from 'autobind-decorator' import pinImage from './images/pin.png' +import { ifIphoneX } from 'react-native-iphone-x-helper' const styles = StyleSheet.create({ container: { @@ -46,24 +47,29 @@ export class Home extends React.Component { } @autobind - handleAdminButton() { - this.props.history.replace('/admin') + handleWorkItemsListPress() { + this.props.history.push('/workitemlist') } @autobind handleItemSelect(item, index) { - this.props.history.replace('/activity') + this.props.history.push('/activity') } @autobind - handleLogout() { + handleLogoutPress() { this.props.history.replace('/logout') } + @autobind + handleGlassesPress() { + this.props.history.push('/arviewer') + } + render() { return ( -
+
{item.title} {item.location} - (this._handleItemSelect(item, index))} > + (this.handleItemSelect(item, index))} > @@ -116,12 +122,13 @@ export class Home extends React.Component { width: '100%', height: 45, backgroundColor: '#F4F4F4', + ...ifIphoneX({ marginBottom: 22 }, {}), }}> - + Hide List - + diff --git a/mobile/src/WorkItem/WorkItem.js b/mobile/src/WorkItem/WorkItem.js index fd09f6e..54dc9ac 100644 --- a/mobile/src/WorkItem/WorkItem.js +++ b/mobile/src/WorkItem/WorkItem.js @@ -1,32 +1,113 @@ import React from 'react' -import { StyleSheet, View, TouchableOpacity, Image, ScrollView, Picker, Text } from 'react-native' +import { + StyleSheet, + View, + Image, + ScrollView, + Text, + TextInput, + KeyboardAvoidingView, + 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 autobind from 'autobind-decorator' +import { ifIphoneX, isIphoneX } from 'react-native-iphone-x-helper' + +const styles = StyleSheet.create({ + container: { + flexGrow: 1, + backgroundColor: '#DDDDDD', + }, + panel: { + width: '94%', + backgroundColor: 'white', + alignSelf: 'center', + marginTop: '3%', + shadowColor: 'gray', + shadowOffset: { width: 2, height: 2 }, + shadowRadius: 2, + shadowOpacity: .5, + padding: 10, + }, + label: { + fontSize: 14, + marginBottom: 4, + } +}) export class WorkItem extends React.Component { - static styles = StyleSheet.create({ - container: { - height: '100%', - width: '100%', - backgroundColor: '#DDDDDD', + static bindings = { + location: { + isValid: true, }, - }) + details: { + isValid: true, + } + } constructor(props) { super(props) + this.state = { + binder: new FormBinder({}, WorkItem.bindings), + messageModal: null, + } + } + + @autobind + handleBackPress() { + const { history } = this.props + + if (history.length > 1) { + history.goBack() + } else { + history.replace('/home') + } } render() { + const { binder } = this.state + return ( - - - Work Item - - - - - - - - - ); + +
+ + + + + + + Pictures: + + + + + + + + + + { isIphoneX ? : null } + + + ) } } diff --git a/mobile/src/WorkItem/WorkItemList.js b/mobile/src/WorkItem/WorkItemList.js index 4980bd9..c5d921f 100644 --- a/mobile/src/WorkItem/WorkItemList.js +++ b/mobile/src/WorkItem/WorkItemList.js @@ -1,5 +1,6 @@ import React from 'react' import { StyleSheet, View, TouchableOpacity, Image, FlatList, Text} from 'react-native' +import { Icon, Header } from '../ui' import autobind from 'autobind-decorator' const styles = StyleSheet.create({ @@ -37,35 +38,51 @@ const inspectionTypes = { export class WorkItemList extends React.Component { constructor(props) { super(props) - this._handleItemSelect = this._handleItemSelect.bind(this) } @autobind - _handleItemSelect(item, index) { + handleItemSelect(item, index) { this.props.history.push('/activity') } + @autobind + handleAddPress(item, index) { + this.props.history.push('/workitem') + } + + @autobind + handleBackPress() { + const { history } = this.props + + if (history.length > 1) { + history.goBack() + } else { + history.replace('/home') + } + } + render() { return ( - { - return ( - - {item.state.toUpperCase()} - - {Admin.inspectionTypes[item.type].title} - {item.location} +
+ { + return ( + + {item.state.toUpperCase()} + + {inspectionTypes[item.type].title} + {item.location} + + (this._handleItemSelect(item, index))} > + + - (this._handleItemSelect(item, index))} > - - - - ) - }} /> + ) + }} /> - ); + ) } } diff --git a/mobile/src/app.js b/mobile/src/app.js index 244baf4..3e658d1 100644 --- a/mobile/src/app.js +++ b/mobile/src/app.js @@ -25,7 +25,7 @@ export default class App extends React.Component { - + diff --git a/mobile/src/ui/BoundOptionStrip.js b/mobile/src/ui/BoundOptionStrip.js new file mode 100644 index 0000000..0565e80 --- /dev/null +++ b/mobile/src/ui/BoundOptionStrip.js @@ -0,0 +1,53 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { View, Text } from 'react-native' +import { OptionStrip } from '.' +import autobind from 'autobind-decorator' + +export class BoundOptionStrip extends React.Component { + static propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string, + binder: PropTypes.object.isRequired, + options: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.string, text: PropTypes.string })).isRequired, + } + + constructor(props) { + super(props) + this.state = props.binder.getFieldState(props.name) + } + + @autobind + handleValueChange() { + const { binder, name } = this.props + const state = binder.getFieldState(name) + + if (!state.readOnly && !state.disabled) { + this.setState(binder.updateFieldValue(name, !state.value)) + } + } + + componentWillReceiveProps(nextProps) { + if (nextProps.binder !== this.props.binder) { + this.setState(nextProps.binder.getFieldState(nextProps.name)) + } + } + + render() { + const { name, label, options } = this.props + const { visible, disabled, value } = this.state + + return ( + + {label} + + + ) + } +} diff --git a/mobile/src/ui/BoundSwitch.js b/mobile/src/ui/BoundSwitch.js index d032c9a..8902cd1 100644 --- a/mobile/src/ui/BoundSwitch.js +++ b/mobile/src/ui/BoundSwitch.js @@ -16,12 +16,12 @@ export class BoundSwitch extends React.Component { } @autobind - handleValueChange() { + handleValueChange(newValue) { const { binder, name } = this.props const state = binder.getFieldState(name) if (!state.readOnly && !state.disabled) { - this.setState(binder.updateFieldValue(name, !state.value)) + this.setState(binder.updateFieldValue(name, newValue)) } } diff --git a/mobile/src/ui/Header.js b/mobile/src/ui/Header.js index 5d84c0d..573d06a 100644 --- a/mobile/src/ui/Header.js +++ b/mobile/src/ui/Header.js @@ -13,6 +13,11 @@ export class Header extends Component { rightButton: PropTypes.shape(headerButtonShape), } + static defaultProps = { + rightButton: { icon: 'none' }, + leftButton: { icon: 'none' }, + } + render() { const { title, leftButton, rightButton } = this.props diff --git a/mobile/src/ui/Icon.js b/mobile/src/ui/Icon.js index b2022e2..218ad83 100644 --- a/mobile/src/ui/Icon.js +++ b/mobile/src/ui/Icon.js @@ -1,5 +1,5 @@ import React, { Component } from 'react' -import { Image } from 'react-native' +import { Image, View } from 'react-native' import PropTypes from 'prop-types' const images = { @@ -11,6 +11,8 @@ const images = { rightArrow: require('./images/right-arrow.png'), search: require('./images/search.png'), settings: require('./images/settings.png'), + add: require('./images/add.png'), + done: require('./images/done.png'), } export class Icon extends Component { @@ -27,11 +29,14 @@ export class Icon extends Component { } render() { - let { size, name, margin, style } = this.props - let source = images[name] || images['hand'] + let { name, margin, style } = this.props + let size = this.props.size - (margin * 2) + let source = images[name] - size -= margin * 2 - - return + if (!source) { + return + } else { + return + } } } diff --git a/mobile/src/ui/OptionStrip.js b/mobile/src/ui/OptionStrip.js new file mode 100644 index 0000000..60b1bf2 --- /dev/null +++ b/mobile/src/ui/OptionStrip.js @@ -0,0 +1,70 @@ +import React, { Component } from 'react' +import { View, Text, TouchableHighlight } from 'react-native' +import PropTypes from 'prop-types' +import autobind from 'autobind-decorator'; + +export class OptionStrip extends Component { + static propTypes = { + options: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.string, text: PropTypes.string })).isRequired, + value: PropTypes.string.isRequired, + onValueChanged: PropTypes.func, + } + + constructor(props) { + super(props) + this.state = { + selectedOption: this.getSelectedOption(props.options, props.value) + } + } + + @autobind + getSelectedOption(options, value) { + return options.find((option) => (value === option.value)) || options[0] + } + + componentWillReceiveProps(newProps) { + if (newProps.options !== props.options || newProps.value !== props.value) { + this.setState({ selectedIndex: this.getSelectedIndex(newProps.options, newProps.value)}) + } + } + + @autobind + handlePress(option) { + const { onValueChanged } = this.props + + this.setState({ selectedOption: option }) + if (onValueChanged) { + onValueChanged(option.value) + } + } + + render() { + const { style, options, value } = this.props + const { selectedOption } = this.state + + return ( + + {options.map((option, index) => ( + this.handlePress(option)}> + + {option.text} + + + ))} + + ) + } +} diff --git a/mobile/src/ui/PhotoButton.js b/mobile/src/ui/PhotoButton.js new file mode 100644 index 0000000..46f87e4 --- /dev/null +++ b/mobile/src/ui/PhotoButton.js @@ -0,0 +1,31 @@ +import React from 'react' +import { + StyleSheet, + View, + Image, + TouchableOpacity +} from 'react-native' +import { Icon } from '.' +import autobind from 'autobind-decorator' + +// const styles = StyleSheet.create() + +export class PhotoButton extends React.Component { + render() { + return ( + + + + + + ) + } +} diff --git a/mobile/src/ui/images/add.png b/mobile/src/ui/images/add.png new file mode 100644 index 0000000..ba03b82 Binary files /dev/null and b/mobile/src/ui/images/add.png differ diff --git a/mobile/src/ui/images/done.png b/mobile/src/ui/images/done.png new file mode 100644 index 0000000..5afda9f Binary files /dev/null and b/mobile/src/ui/images/done.png differ diff --git a/mobile/src/ui/index.js b/mobile/src/ui/index.js index ca42c78..4d00679 100644 --- a/mobile/src/ui/index.js +++ b/mobile/src/ui/index.js @@ -1,5 +1,8 @@ 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'